ly2011 / blog Goto Github PK
View Code? Open in Web Editor NEW前端学习笔记
Home Page: https://ly2011.github.io/blog
前端学习笔记
Home Page: https://ly2011.github.io/blog
在 websocket 连接被建立后,如果一段时间未活动,服务器或防火墙可能会超时或终止连接。想要解决这个问题, 我们可以周期性地给服务器发消息。我们需要两个方法实现:一个来确保连接不会中断,,另一个用来取消此设定。同我们也需要一个 timerID 变量.
让我们来看一下具体实现:
var timerID = 0;
function keepAlive() {
var timeout = 20000;
if (webSocket.readyState == webSocket.OPEN) {
webSocket.send('');
}
timerId = setTimeout(keepAlive, timeout);
}
function cancelKeepAlive() {
if (timerId) {
clearTimeout(timerId);
}
}
现在我们实现了我们需要的两个方法,我们可以在 onOpen()
的最后面调用 keepAlive()
,在 onClose()
的组后面调用 cancelKeepAlive()
。
方法1:
.box:before {
position: absolute;
top: 0;
left: 0;
width: 100%;
right: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
background-color: #e2e2e2;
}
方法2:
.box:before{
content: "";
pointer-events: none; /* 防止点击触发 */
box-sizing: border-box;
position: absolute;
width: 200%;
height: 200%;
left: 0;
top: 0;
border:1px solid #000;
-webkit-transform(scale(0.5));
-webkit-transform-origin: 0 0;
transform(scale(0.5));
transform-origin: 0 0;
}
缺点:
问题: 在第一张或者最后一张图片切换时,会显示空白
解决方案: 使用无限滚动的技巧,即实现循环无缝切换。
1)在页面图片列表的开始加上最后一张图片的附属图,在最后加上第一张图片的附属图
2)每次切换时判断切换后的位置是否大于 -1610px 或是小于 -4830px (加上附属图总大小是 8050, 每张大小是 1610px, 总5张), 即是否切换到附属图的位置:
如果大于 -1610px, 就把图片重新定位到真正的最后一张图的位置:-4830px;
如果小于 -4830px, 就把图片重新定位到真正的第一张图的位置:-1610px;
html:
如果你定义了一个数组,然后你想清空它。 通常,你会这样做:
// 定义一个数组
var list = [1, 2, 3, 4];
function empty() {
//清空数组
list = [];
}
empty();
但是,这有一个效率更高的方法来清空数组。 你可以这样写:
var list = [1, 2, 3, 4];
function empty() {
//empty your array
list.length = 0;
}
empty();
list = []
将一个新的数组的引用赋值给变量,其他引用并不受影响。 这意味着以前数组的内容被引用的话将依旧存在于内存中,这将导致内存泄漏。
list.length = 0
删除数组里的所有内容,也将影响到其他引用。
然而,如果你复制了一个数组(A 和 Copy-A),如果你用 list.length = 0
清空了它的内容,复制的数组也会清空它的内容。
考虑一下将会输出什么:
var foo = [1,2,3];
var bar = [1,2,3];
var foo2 = foo;
var bar2 = bar;
foo = [];
bar.length = 0;
console.log(foo, bar, foo2, bar2);
//[] [] [1, 2, 3] []
React.Children
是顶层API之一,为处理 this.props.children
这个封闭的数据结构提供了有用的工具。
this.props
对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children
属性。它表示组件的所有子节点。
1、React.Children.map
object React.Children.map(object children, function fn [, object context])
使用方法:
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
其他方法
this.props.children.forEach(function (child) {
return <li>{child}</li>
})
在每一个直接子级(包含在 children 参数中的)上调用 fn 函数,此函数中的 this 指向上下文。如果 children 是一个内嵌的对象或者数组,它将被遍历:不会传入容器对象到 fn 中。如果 children 参数是 null 或者 undefined,那么返回 null 或者 undefined 而不是一个空对象。
function Title({text, children}) {
console.log('children: ', children, React.Children.count(children))
React.Children.map(children, function (child) {
console.log('child: ', child)
return child
})
return (
<h1>
{ text }
{ children }
</h1>);
}
React.render(
<Title>
<span>hello</span>
<span>hello</span>
</Title>,
document.body
);
这里需要注意, this.props.children
的值有三种可能:如果
当前组件没有子节点,它就是 undefined
;如果有一个子节点,数据类型就是 object
;如果有多个节点,数据类型就是 array
。所以
,处理 this.props.children
的时候要小心。
React
提供了一个工具方法 React.Children
来处理 this.props.children
。我们可以用 React.Children.map
来遍历子节点,而不用担心 this.props.children
的数据类型是 undefined
还是 object
。
传入如下 ReactElement:
<Title>
<span>hello</span>
<span>hello</span>
</Title>
// 返回两个子节点,数据类型是 `array`
<Title></Title>
// 返回 undefined
<Title>
null
</Title>
// 返回 null
2、React.Children.forEach
React.Children.forEach(object children, function fn [, object context])
类似于 React.Children.map()
, 但是不返回对象。
3、React.Children.count
number React.Children.count(object children)
返回 children 当中的组件总数,和传递给 map 或者 forEach 的回调函数的调用次数一致。
不同的 ReactElement, 输出 count 值:
<Title>
<span>hello</span>
<span>hello</span>
</Title>
console.log(React.Children.count(this.props.children)) // 2
<Title></Title>
console.log(React.Children.count(this.props.children)) // 0
<Title>null</Title>
console.log(React.Children.count(this.props.children)) // 1
4、React.Children.only
object React.Children.only(object children)
返回 children
中仅有的子级。否则抛出异常。
这里仅有的子级,only方法接收的参数只能是一个对象,不能说多个对象(数组)。
console.log(React.Children.only(this.props.children[0]))
// 输出对象 this.props.children[0]
解决方案:给 $route.query
设置一个任意值即可,如:
const qparams = this.$route.query;
qparams.tmp_randomTime = isNaN(parseInt(qparams.tmp_randomTime))
? ''
: parseInt(qparams.tmp_randomTime);
el-pagination
中的 page无法正确高亮解决方案:total(总条数)默认值必须设置为 null
。例如:
searchForm: {
page: 1,
size: 10,
total: null
},
解决方案:在列表页跳转前,在params中携带 scrollTop
参数,返回时再返回即可。例如:
params.scrollTop =
document.querySelector('.main-container').scrollTop || 0;
el-form
内嵌 vue-preview
会导致vue-preview点击关闭按钮或者切换到下一张图片时路由切换el-form
内嵌 vue-preview
, 并且配置 vue-preview
的参数 history: false
。Vue.use(VuePreview, {
mainClass: 'pswp--minimal--dark',
barsSize: {
top: 0,
bottom: 0
},
captionEl: false,
fullscreenEl: true,
shareEl: false,
bgOpacity: 0.85,
tapToClose: false,
tapToToggleControls: false,
history: false
});
解决方案:修改 el-dropdown
的 hide()
方法
hide () {
if (this.triggerElm.disabled) return;
// console.log('dropdown_showDatePicker: ', this.showDatePicker)
if (this.showDatePicker) return; // 展开事件组件时,禁止关闭下拉弹框
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.visible = false;
}, this.hideTimeout);
},
详情:做列表搜索的时候当表单只有单个输入框时,回车会自动提交表单
解决:(组织表单提交)
<el-form :inline="true" :model="params" @submit.native.prevent>
</el-form>
<el-input v-on:keyup.enter.native="login"></el-input>
解决:但是我在Stack Overflow找了个更简单的方法,要直接操作table树๑乛◡乛๑,只需要用到expand的方法
<el-table @expand="handleExpandRow" ref="row_table">
</el-table>
//method:
handleExpandRow(row,expanded){
this.$refs.row_table.store.states.expandRows = expanded ? [row] : [];
}
附上Stack Overflow原地址:大神在此
————
以上是v1.4版本的,由于v2.0版本修改了返回参数,仿照上例修改了新的:
handleExpandRow(row,expandRows){
this.$refs.raw_table.store.states.expandRows = expandRows.length !== 0 ? [row] : [];
}
@include
和 @extend
的区别(sass)/* 1. 添加新元素 */
<div class="outer">
<div class="div1"></div>
<div class="div2"></div>
<div class="div3"></div>
<div class="clearfix"></div>
</div>
.clearfix {
clear: both;
}
/* 2. 为父元素增加样式 */
.clearfix {
overflow: auto;
zoom: 1; // 处理兼容性
}
/* 3. :after 伪元素方法(作用于父元素) */
.outer {
zoom: 1;
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
方法1:
.box:before {
position: absolute;
top: 0;
left: 0;
width: 100%;
right: 0;
height: 1px;
content: '';
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
background-color: #e2e2e2;
}
方法2:
.box:before{
content: "";
pointer-events: none; /* 防止点击触发 */
box-sizing: border-box;
position: absolute;
width: 200%;
height: 200%;
left: 0;
top: 0;
border:1px solid #000;
-webkit-transform(scale(0.5));
-webkit-transform-origin: 0 0;
transform(scale(0.5));
transform-origin: 0 0;
}
缺点:
.eight {
position: absolute;
width: 80px;
height: 80px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
background: green;
}
这个方法可以不需要设定固定的宽高,在移动端用的会比较多,在移动端css3兼容的比较好
创建Ajax的过程:
1)创建XMLHttpRequest对象(异步调用对象)
var xhr = new XMLHttpRequest();
2)创建新的Http请求(方法、URL、是否异步)
xhr.open('get', 'example.php', false);
3)设置响应HTTP请求状态变化的函数
onreadystatechange事件中readyState属性等于4。响应的HTTP状态为 200(OK)或者304(Not Modified)。
4)发送http请求
xhr.send(data);
5)获取异步调用返回的数据
详细过程:
const xhr = new XMLHttpRequest();
xhr.open(method, url, async);
// send 方法发送请求,并接受一个可选参数
// 当请求方式为 post 时,可以将请求体的参数传入
// 当请求方式为 get 时,可以不传或传入 null
// 不管是 get 还是 post,参数都需要通过 encodeURIComponent 编码后拼接
xhr.send(data);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
// HTTP 状态在 200 -300 之间表示请求成功
// HTTP 状态为 304 表示内容未发生改变,可直接从缓存中获取
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log('请求成功', xhr.responseText);
}
}
}
// 超时时间单位为毫秒
xhr.timeout = 1000
// 当请求超时时,会触发 ontimeout 方法
xhr.ontimeout = () => console.log('请求超时')
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 }
console.log(a.x); // undefined
console.log(b.x); // {n:2}
解析:
解析器在接收到 a = a.x = { n: 2 }
这个语句后,会这样子做:
在找到 a 和 a.x 的指针。如果已有指针,那么不改变它。如果没有指针, 即那个变量还没被申明,那么就创建它,指向 null。
a 是有指针的,指向 {n: 1}
; a.x 是没有指针的,所以创建它,指向 null。
然后把上面找到的指针,都指向最右赋值的那个指,即是 {n: 2}
。
浅拷贝:
slice
实现var arr = ['old', '1' , true, null, undefined];
var new_arr = arr.slice();
new_arr[0] = 'new';
console.log(arr);
console.log(new_arr);
concat
实现var arr = ['old', '1' , true, null, undefined];
var new_arr = arr.concat();
new_arr[0] = 'new';
console.log(arr);
console.log(new_arr);
注释:如果数组元素是基本类型,就会拷贝一份新的;但是如果数组元素是对象或者数组,就只会拷贝引用(类似指针),修改其中一个就会影响另外一个。例如:
var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}];
var new_arr = arr.concat();
new_arr[0] = 'new';
new_arr[3][0] = 'new1';
console.log(arr) // ["old", 1, true, ['new1', 'old2'], {old: 1}]
console.log(new_arr) // ["new", 1, true, ['new1', 'old2'], {old: 1}]
深拷贝
JSON.stringify
实现数组深拷贝var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}];
var new_arr = JSON.parse(JSON.stringify(arr));
new_arr[0] = 'new';
new_arr[3][0] = 'new1';
console.log(arr) // ["old", 1, true, ['old1', 'old2'], {old: 1}]
console.log(new_arr) // ["new", 1, true, ['new1', 'old2'], {old: 1}]
缺点:简单粗暴,但是不能拷贝函数,不推荐。
手动实现深浅拷贝
var shallowCopy = function (obj) {
// 判断是否是数组或者对象
if (typeof obj !== 'object') {
return obj;
}
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj;
}
var deepCopy = function (obj) {
// 判断是否是数组或者对象
if (typeof obj !== 'object') {
return obj;
}
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
// 创建节点
createDocumentFragment()
createElement()
createTextNode()
// 添加 移除 替换 插入
appendChild()
removeChild()
replaceChild()
insertBefore()
// 查找
document.getElementsByTagName()
document.getElementsByName()
document.getElementsByClassName()
document.getElementById()
document.querySelector()
document.querySelectorAll()
- 视图(View):用户界面。
- 控制器(Controller):业务逻辑。
- 模型(Model):数据保存。
各部分之间的通讯方式如下:
1、View 传送指令到 Controller
2、Controller 完成业务逻辑后,要求 Model 改变状态
3、Model 将新的数据发送到 View,用户得到反馈
特点:
所有通讯都是单向的。
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
1、各部分之间的通讯,都是双向的。
2、View 与 Model 不发生联系,都通过 Presenter 传递。
3、View 非常薄,不部署任何业务逻辑,被称为 “被动视图”(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在那里。
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反应在 ViewModel,反之亦然。
function trim(str) {
return str.replace(/(^\s*)|(\s*$)/g, '');
}
BOM对象:
DOM对象
常用的DOM方法:
常用的DOM属性:
参考: HTML DOM 方法
下面代码输出结果?为什么?
demo1:
setTimeout(() => console.log('a'), 0);
var p = new Promise((resolve) => {
console.log('b');
resolve();
});
p.then(() => console.log('c'));
p.then(() => console.log('d'));
console.log('e');
// 结果:b e c d a
// 任务队列优先级:promise.Trick()>promise的回调>setTimeout>setImmediate
demo2:
async function async1() {
console.log("a");
await async2(); //执行这一句后,await会让出当前线程,将后面的代码加到任务队列中,然后继续执行函数后面的同步代码
console.log("b");
}
async function async2() {
console.log( 'c');
}
console.log("d");
setTimeout(function () {
console.log("e");
},0);
async1();
new Promise(function (resolve) {
console.log("f");
resolve();
}).then(function () {
console.log("g");
});
console.log('h');
// 谁知道为啥结果不一样?????????????
// 直接在控制台中运行结果: d a c f h g b e
// 在页面的script标签中运行结果:d a c f h b g e
<ul id="ui-test">
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
<li>item5</li>
<li>item6</li>
<li>item7</li>
<li>item8</li>
<li>item9</li>
<li>item10</li>
</ul>
<script>
document.addEventListener('DOMContentLoaded', function(event) {
var oUL = document.querySelector('#ui-test')
// var oLI = document.querySelectorAll('#ui-test > li')
// for (var i = 0, len = oLI.length; i < len; i++) {
// console.log(oLI[i]);
// oLI[i].addEventListener('click', function() {
// console.log(this.innerHTML)
// })
// }
// 代理模式
oUL.addEventListener('click', function(ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() === 'li') {
console.log(target);
console.log(target.innerHTML)
}
})
});
</script>
indexOf
判断数组元素第一次出现的位置是否为当前位置Set
var arr = [6, 4, 1, 8, 2, 11, 3];
function max (prev, next) {
return Math.max(prev, next)
}
console.log(arr.reduce(max));
var arr = [6, 4, 1, 8, 2, 11, 3];
console.log(Math.max.apply(null, arr)); // 11
var arr = [6, 4, 1, 8, 2, 11, 3];
function max (arr) {
return Math.max(...arr);
}
console.log(max(arr));
var arr = [];
for(var i = 0; i < 100; i++) {
arr[i] = i;
}
arr.sort(function() {
return 0.5 - Math.random();
})
console.log(arr)
var arr = [1, [2, [3, 4]]]
function flatten(arr) {
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4]
var TemplateEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g,
match;
while(match = re.exec(tpl)) {
console.log('match0: ', match[0]);
console.log('match1: ', match[1]);
tpl = tpl.replace(match[0], data[match[1]])
}
return tpl;
}
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.<p>';
console.log(TemplateEngine(template, {
name: 'ly2011',
age: 16
}))
函数柯里化:在一个函数中首先填充几个参数(然后再返回一个新函数)的技术称为柯里化(Currying)。
// 通用的函数柯里化构造方法
function curry(fn) {
// 新建 args 保存参数,注意,第一个参数应该是要科利华的函数,所以 args 里面去掉第一个
var args = [].slice.call(arguments, 1);
/* // 新建 _fn 函数作为返回值
var _fn = function() {
// 参数长度为 0, 执行 fn 函数,完成该函数的功能
if(arguments.length === 0) {
return fn.apply(this, args)
} else {
// 否则,存储参数到闭包中,返回本函数
[].push.apply(this, arguments);
return _fn;
}
}
return _fn;*/
return function() {
var newArgs = args.concat([].slice.call(arguments))
return fn.apply(null, newArgs)
}
}
function add() {
return [].reduce.call(arguments, function(a, b) {
return a + b;
} )
}
function multiply() {
return [].reduce.call(arguments, function(a, b) {
return a * b;
})
}
console.log(curry(add, 1, 2, 3, 4, 5)(1)) // 16
console.log(curry(multiply, 1, 2, 3, 4)(5)) // 120
function multiply(x, y) {
if (y === undefined) {
return function(z) {
return x * z
}
} else {
return x * y
}
}
console.log(multiply(3, 4)); // 12
console.log(multiply(3)(4)); // 12
柯里化的作用
websocket协议、localstorage、以及使用 html5浏览器的新特性 SharedWorker。
cookie
是网站为了标示用户身份而储存在用户本地终端(Client Slide) 上的数据(通常经过加密)cookie
数据始终在同源的 http 请求中携带(即使不需要),既会在浏览器和服务器间来回传递sessionStorage
和 localStorage
不会自动把数据发给服务器,仅在本地保存存储大小:
cookie
数据大小不能超过 4k
sessionStorage
和 localStorage
虽然也有存储大小的限制,但比 cookie
大得多,可以达到 5M 或 更大有效时间:
localStorage
存储持久数据,浏览器关闭后数据不丢失除非浏览器主动删除数据。sessionStorage
数据在当前浏览器窗口关闭后自动删除cookie
设置的 cookie
过期时间之前一直有效,即使窗口或浏览器关闭for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
结果为: 5,5,5,5,5
解决办法
for(var i = 0; i < 5; i++) {
(function(j) { // j = i
setTimeout(function() {
console.log(j)
}, 1000)
})(i)
}
var output = function(i) {
setTimeout(function() {
console.log(i)
}, 1000)
}
for(var i = 0; i < 5; i++) {
output(i) // 这里传过去的 i 被复制了
}
for(let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000);
}
如果要让0-4一秒一秒地输出来呢?
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j)
}, 1000 * j ) // 这里修改 0-4 的定时器时间
})(i)
}
第二种:
const tasks= []
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(i)
resolve() // 这里一定要 resolve, 否则代码不会按预期 work
}, 1000 * i)
})
// 生成全部的异步操作
for(var i = 0; i < 5; i++) {
tasks.push(output(i))
}
// 异步操作完成之后,输出最后的i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i)
}, 1000)
})
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 声明即执行的 async 函数表达式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
1)this总是指向函数的直接调用者(而非间接调用者)
2)如果有new关键字,this指向new出来的那个对象
3)在事件中,this指向目标元素,特殊的是IE的attachEvent中的this总是指向全局对象window。
它的功能是把对应的字符串解析成JS代码并运行;应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)。
[1, NaN, NaN]
解析:
Array.prototype.map()
array.map(callback[, thisArg])
callback函数的执行规则
参数:自动传入三个参数
currentValue(当前被传递的元素)
index(当前被传递的元素的索引)
array(调用map方法的数组)
parseInt
parseInt方法接收两个参数
第三个参数['1', '2', '3']将被忽略。parseInt方法将会通过以下方式被调用
parseInt('1', 0)
parseInt('2', 1)
parseInt('3', 2)
parseInt的第二个参数radix为0时,ECMAScript5将string作为十进制数字的字符串解析。
parseInt的第二个参数radix为1时,解析结果为NaN;
parseInt的第二个参数radix在2-36之间时,如果string参数的第一个字符(除空白以外),不属于radix指定进制下的字符,解析结果为NaN。
parseInt('3', 2)执行时,由于 '3' 不属于二进制字符,解析结果为 NaN。
闭包指的是一个函数可以访问另一个函数作用域中变量。常见的构造方法,是在一个函数内部定义另外一个函数。内部函数可以引用外层的变量;外层变量不会被垃圾回收机制回收。
注意:闭包的原理是作用域链,所以闭包访问的上级作用域中的变量是个对象,其值为其运算结束后的左后一个值。
优点:避免全局变量污染。
缺点:容易造成内存泄漏。
例子:
function makeFunc() {
var name = 'Mozilla'
function displayName() {
console.log(name)
}
return displayName
}
var myFunc = makeFunc()
myFunc() // 输出Mozilla
myFunc变成一个闭包。闭包是一种特殊的对象。它由两部分构成: 函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。在我们的例子中,myFunc是一个闭包,由displayName函数和闭包创建时存在的 'Mozilla' 字符串组成。
defer和async、动态创建DOM方式、按需异步载入JS
defer: 延迟脚本。立即下载,但延迟执行(延迟到整个页面都解析完毕后再运行),按照脚本出现的先后顺序执行。
async: 异步脚本。下载完立即执行,但不保证按照脚本出现的先后顺序执行。
若请求的资源编码,如外引js文件编码与页面编码不同。可根据外引资源编码方式定义为 charset="utf-8"或"gbk"。
比如:http://www.yyy.com/a.html 中嵌入了一个http://www.xxx.com/test.js
a.html 的编码是gbk或gb2312的。 而引入的js编码为utf-8的 ,那就需要在引入的时候
渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进,达到更好的用户体验。
优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
优点:
缺点:
for
forEach
map
filter
some
对象循环:
for ... in
原理:
触发重排
页面布局和元素几何属性的改变就会导致重排
下列情况会发生重排
以下属性或方法会刷新渲染队列
减少重绘和重排的原理很简单:
主要过程是:
浏览器解析->查询缓存->dns查询->建立链接->服务器处理请求->服务器发送响应->客户端收到页面->解析HTML->构建渲染树->开始显示内容(白屏时间)->首屏内容加载完成(首屏时间)->用户可交户(DOMContentLoaded)->加载完成(load)
缓存
,如果请求资源在缓存中并且新鲜,跳转到转码步骤
Expires
和 Cache-Control
:
script
,meta
这样本身不可见的标签。2)被css隐藏的节点,如display: none
HTTP
请求:合并文件、CSS
精灵、inline Image
DNS
查询: DNS
缓存、将资源分布到恰当数量的主机名DOM
元素数量CDN
ETag
Gzip
压缩Cookie
大小解析:
1. 比较相邻的两个元素,如果前一个比后一个大,则交换位置。
2. 第一轮的时候最后一个元素应该是最大的一个。
3. 按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。
function sort(elements) {
for(var i = 0; i < elements.length - 1; i++) {
for(var j = 0; j < elements.length - i - 1; j++) {
if (elements[j] > elements[j + 1]) {
var swap = elements[j];
elements[j] = elements[j + 1];
elements[j + 1] = swap;
}
}
}
}
var elements = [3, 1, 5, 7, 2, 4, 9, 6, 10, 8];
console.log('before: ', elements)
sort(elements)
console.log('after: ', elements)
解析:
快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。然后递归调用,在两边都实行快速排序。
function quickSort(elements) {
if (elements.length <= 1) {
return elements;
}
var pivotIndex = Math.floor(elements.length / 2);
var pivot = elements.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < elements.length; i++) {
if (elements[i] < pivot) {
left.push(elements[i])
} else {
right.push(elements[i])
}
}
return quickSort(left).concat([pivot] , quickSort(right))
}
var elements = [5, 6, 2, 1, 3, 8, 7, 1.2, 5.5, 4.5]
console.log(quickSort(elements))
例如:sdddrtkjsfkasjdddj中出现最多的字符是d,出现了6次
var str = "sdddrtkjsfkkkasjdddj";
var max = 0;
var char;
function Search(str) {
var json = {};
for (var i = 0; i < str.length; i++) {
if (!json[str[i]]) {
json[str[i]] = str[i];
} else {
json[str[i]] += str[i];
}
}
for (var i = 0; i < str.length; i++) {
if (json[str[i]].length > max) {
max = json[str[i]].length;
char = str[i];
}
}
console.log('json: ', json)
console.log("出现次数最多的字符是" + char + ",出现了" + max + "次")
}
Search(str);
// 计算数组中每个元素出现的次数
var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']
var countedNames = names.reduce(function(allNames, name) {
if(name in allNames) {
allNames[name]++
} else {
allNames[name] = 1
}
return allNames;
}, {})
console.log(countedNames)
var sum = 0;
for(var i=0,j=0;i<10,j<6;i++,j++) {
sum = i+j
}
console.log(sum)
解析:
答案是: 10, 首先每次for循环的i和j值是相等的:
第一次:i=0, j =0; 符合条件, sum = i + j = 0;
第二次:i=1,j=1;符合条件, sum = i + j = 2;
第三次:i=2,j=2;符合条件, sum = i + j = 4;
第四次:i=3,j=3;符合条件,sum = i + j = 6;
第五次:i=4,j=4;符合条件, sum = i + j = 8;
第六次:i=5,j=5;符合条件,sum = i + j =10;
第七次:i=6,j=6;不符合条件((这里需要注意,循环继续的判断依据以分号前的最后一项为准,即判断j<6符不符合条件),循环结束。
注意:
这里值得一提的是 如果把条件i<10,j<6;改成i<6,j<10;
结果将完全不同,此时循环执行到j<10才会结束,此时sum=18。
参考地址
二者的区别是 mouseenter 不会冒泡(bubble)。
详细解析一下:
当两者绑定的元素都没有子元素时,两者的行为是一直的。但是当两者内部都包含子元素时,行为就不一样了。
在 mouseover 绑定的元素中,鼠标每次进入一个子元素就会触发一次 mouseover 事件,而 mouseenter 只会触发一次。
mouseover 事件对应 mouseout事件
mouseenter 事件对应 mouseleave 事件
demo:
<style type="text/css" media="screen">
* {
margin: 0;
padding: 0;
}
.container {
overflow: hidden;
}
.over {
background-color: lightgray;
padding: 20px;
width: 40%;
float: left;
}
.enter {
background-color: lightgray;
padding: 20px;
width: 40%;
float: right;
}
.over h2, .enter h2 {
background-color: #fff;
}
</style>
</head>
<body>
<div class="container">
<div class="over">
<!-- <h2 class="over-counter"></h2> -->
</div>
<div class="enter">
<!-- <h2 class="enter-counter"></h2> -->
</div>
</div>
<script type="text/javascript">
function $(ele) {
return document.querySelector(ele)
}
var x = 0,
y = 0;
var $over = $('.over');
var $enter = $('.enter');
$over.addEventListener('mouseover', function(e) {
$over.innerText = ++x;
// $('.over-counter').innerText = ++x;
});
$enter.addEventListener('mouseenter', function(e) {
$enter.innerText = ++y;
// $('.enter-counter').innerText = ++y;
});
</script>
</body>
移动端的click事件会延迟300ms触发事件回调(只在部分手机浏览器上出现)。
解决办法:
引入 fastlick.js 来解决。它的原理是 fastlick 在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即触发一个模拟 click 事件,并把浏览器在 300 毫秒之后真正触发的click事件阻止掉。
currentTarget是当前事件遍历DOM时,标识事件的当前目标。它总是引用事件处理程序附加到的元素(事件代理对象上),而不是 event.target, event.target标识事件发生的元素。
有个简单的验证方法,你会在下面的例子中看到 e.currentTarget 一直返回的是 body元素,而e.target则随着你点击的位置的不同而变化。
<body>
<ul id="test">
<li>
<ul class="enter-sensitive">
<li>item 1-1</li>
<li>item 1-2</li>
</ul>
</li>
<li>
<ul class="enter-sensitive">
<li>item 2-1</li>
<li>item 2-2</li>
</ul>
</li>
</ul>
<script>
document.body.addEventListener('click', function (e) {
console.log(e.target, e.currentTarget)
})
</script>
</body>
事件冒泡是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接受,然后逐级向上传播到较为不具体的节点(文档)。
阻止事件冒泡的方法:
调用当前时间对象的 stopPropagation()
方法
IE10及其以下 cancelBubble = true
function cancelBubble(e) {
var evt = e ? e : window.event;
if (evt.stopPropagation) { // W3C
evt.stopPropagation();
}else { // IE
evt.cancelBubble = true;
}
}
阻止默认事件:
调用当前事件对象的 preventDefault()
方法
IE下:returnValue = false
function preventDefaultAction(event){
var event = window.event || event;
if(document.all){
// 支持IE
event.returnValue = false;
}else{
// IE不支持
event.preventDefault();
}
}
点击穿透是指在移动端,由于click事件延迟300ms触发,那么如果300ms内,页面显示变化(主要是指DOM的隐藏和显示)的话,会出现实际点击元素触发了touch事件,而300ms后该位置的实际元素又被再次触发了click事件的情况。
避免方法:
引入 fastlick.js
事件委托是指利用 "事件冒泡",止痛膏指定一个事件处理程序,来管理某一类型的所有事件。
在 Elements
标签内,查看页面元素的时候,如果当前这个元素不在视图内,可以通过这个方法让这个元素快速滚入视图中。
操作:
在Network 标签下的所有请求,都可以复制为一个完整的
Fetch` 请求的代码。
操作:
在 Network
标签页下,选中一个请求,右击该请求,选择 Block request domain
或 Block request URL
, 可以分别阻塞该请求在 domain
下的所有请求和该请求。
在debug
的时候,有时候需要在元素的点击事件监听函数中,将该点击事件对象打印出来。有个更方便的方式,是可以直接在 Elements
标签页为页面元素添加事件监听事件。
操作:
Elements
标签页中选中一个页面元素(选中之后,默认可以通过 $0
变量获取到该元素)Console
标签页中,调用函数 monitorEvents
, 输入两个参数,第一个是当前元素($0
), 第二个是事件名(click
)Enter
后,当被选中的元素触发了点击事件之后,Console
标签页会将该点击事件对象打印出来在 Elements
标签页,你可以拖动任何 HTML
元素,改变它在页面中的位置。
操作:如下图。
基本上大家都会用JavaScript
的断点调试,但是应该很多人不知道 DOM
节点也可以进行断点调试。
Chrome DevTools
提供了三种针对 DOM
元素的断点调试:子元素改变时
、属性改变时
和元素被移除时
。
操作:
Elements
标签页,选中一个元素Break on
--> subtree modifications
(或 attribute modifications
或 node removal
)在新版本的 Chrome
中,提供了一个截图的 API
,你可以将整个页面截图或者截取部分页面元素,且截取的图片尺寸跟浏览器当前视图中要截取的内容所占尺寸一致。截图输出的是 png
格式的图片,会自动通过浏览器下载到默认的目录下。现在有三种截取的方式:
截取页面部分元素的操作:
CMD + SHIFT + P
(windows 中用 CTRL + SHIFT + P
) 打开命令菜单Elements
标签页,选中要截取的页面元素Capture node screenshot
截取完整页面的操作
CMD + SHIFT + P
(windows 中用 CTRL+SHIFT+P
)打开命令菜单Capture full size screenshot
(不需要选择页面元素)截取当前视图内的页面
CMD + SHIFT + P
(windows 中用 CTRL + SHIFT + P
)打开命令菜单Capture screenshot
(不需要选择页面元素)在 Chrome DevTools
上运行 JavaScript
表达式的时候,可以使用 $_
来获取到上一步操作的返回值。
在 Chrome DevTools
上调试 CSS
或 JavaScript
, 修改的属性值在重新刷新页面时,所有的修改都会被重置。如果你想把修改的值保存下来,刷新页面的时候不会被重置,那就看看下面这个特性 Overrides
吧。 Overrides
默认是关闭的,需要手动开启,开启的步骤如下。
开启的操作:
Chrome DevTools
的 Sources
标签页Overrides
子标签+Select folder for overrides
, 来为 Overrides
设置一个保存重写属性的目录下面是将多位数组转化为单一数组的三种不同方法:
对于此数组:
var myArray = [[1, 2],[3, 4, 5], [6, 7, 8, 9]];
我们需要的结果是:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
concat()
和 apply()
var myNewArray = [].concat.apply([], myArray)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
reduce()
var myNewArray = myArray.reduce(function (prev, curr) {
return prev.concat(curr)
})
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
var myNewArray3 = [];
for (var i = 0; i < myArray.length; ++i) {
for (var j = 0; j < myArray[i].length; ++j)
myNewArray3.push(myArray[i][j]);
}
console.log(myNewArray3);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
var myNewArray4 = [].concat(...myArray)
console.log(myNewArray4)
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// 使用 Infinity 作为深度,展开任意深度的嵌套数组
arr3.flat(Infinity);
对于无限嵌套的数组请使用 Lodash 的 flattenDeep()。
img标签在HTML5和HTML4.0.1的严格模式渲染的时候,下面会有几像素的空白,如图所示:
图片默认是行内元素(inline-block
),行内元素默认是baseline
(第三条线)对齐的,和底部会有一段距离,如图所示:
解决方法有很多种,根据场景需要选择合适的处理方式
img {
vertical-align: top;
}
.parent {
font-size: 0;
}
img {
display:block;
}
js中存在一个叫做执行栈的东西。JS的所有同步代码都在这里执行,当执行一个函数调用时,会创建一个新的执行环境并压入到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。
这里有一个问题是代码中不只是同步代码,也会有异步代码。当一个异步任务执行完毕后会将任务添加到任务队列中。例如:
setTimeout(_ => {}, 1000)
代码中 setTimeout 会在一秒后将回调函数添加到任务队列中。事实上异步队列也分两种类型:微任务、宏任务。
微任务和宏任务的区别是,当执行栈空了,会检查微任务队列是否有任务,将微任务队列中的任务依次拿出来执行一遍。当微任务队列空了,从宏任务队列中拿出来一个任务去执行,执行完毕后检查微任务队列,微任务队列空了之后再从宏任务队列中拿出来一个任务执行。这样持续的交替执行任务叫做 事件循环。
属于微任务(microtask)的事件有以下几种:
属于宏任务(macrotask)的事件有以下几种:
[TOC]
<!-- Vue1 这么写-->
<li v-for="item in items"> 第 {{$index}} 条: {{item.message}}</li>
<div v-for="item in items" track-by="id"></div>
<!-- Vue2 这么写 -->
<li v-for="(item, index) in items">第 {{index}} 条: {{item.message}}</li>
<div v-for="item in items" v-bind:key="item.id"></div>
<!-- Vue1 这么写 -->
<li v-for="(key, value) in obj"></li>
<!-- Vue2 这么写 -->
<li v-for="(value, key) in obj"></li>
<!-- Vue1 从 0 开始, Vue2 从 1 开始 -->
<span v-for="n in 10">{{n}}</span>
<!-- 如果ok为false, 不输出在HTML中 -->
<div v-if="ok">Yes</div>
<div v-else>No</div>
<!-- 如果ok为false, 只是 display: none 而已 -->
<h1 v-show="ok">Hello!</h1>
<button v-on:click="say('hi')">点击</button>
<!-- 简写 -->
<button @click="say('hi')">点击</button>
<!-- 传入 event 对象 -->
<button @click="say('hi', $event)">点击</button>
<!-- 阻止单击事件冒泡 -->
<button @click.stop="doSth">点击</button>
<!-- 阻止默认行为 -->
<button @submit.prevent="doSth">单击</button>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
<!-- 按键修饰符: 回车才会执行 -->
<input @keyup.13="submit"><!-- 13 为 keycode -->
<input @keyup.enter="submit">
<!-- 支持的全部按钮为 enter, tab, delete, space, up, down, left, right 字母 -->
<input type="text" v-model="message">
<!-- 自定义选中值。否则选中为value值, 不选为空 -->
<input
type="checkbox"
v-model="toggle"
v-bind:true-value="a"
v-bind:false-value="b"
>
<div v-bind:class="{ 'class-a': isA, 'class-b': isB }"></div>
<!-- classArr是一个数组 -->
<div v-bind:class="classArr"></div>
<!-- 简写 -->
<div :class="{ 'class-a': isA, 'class-b': isB }"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<img :src="imgSrc">
<a :href="baseURL + '/path'"></a>
在 Vue2 中, 如果属性值是变量, 必须用绑定属性的写法。
<!-- wrong -->
<img src="{{imgSrc}}">
<!-- right -->
<img :src="imgSrc">
[v-cloak] {
display: none;
}
<div v-cloak>
{{message}}
</div>
不会显示 <div>
的内容, 直到编译结束。
单向绑定的意思是, 即使绑定变量的值发生变化, 显式的内容仍旧就是最初绑定时候的值。
<!-- Vue1 这么写 -->
<span>This will never change: {{* msg}}</span>
<!-- Vue2 不支持 -->
<!-- Vue1 这么写-->
<div>{{{ raw_html }}}</div> <!-- {{}} 中的 HTML 内容会转化为纯文本 -->
<!-- 或者 -->
<div v-html="raw_html"></div>
<!-- Vue2 这么写 -->
<div v-html="raw_html"></div>
<div id="demo">{{ fullName }}</div>
new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Vue.directive('my-directive', {
bind: function () {
// 准备工作
// 例如, 添加事件处理器或只需要运行一次的高耗任务
this.el; // 添加指令的元素
this.vm.$get(name); // 或得该指令的上下文 ViewModel
this.expression; // 指令的表达式的值
},
update: function (newValue, oldValue) {
// 值更新时的工作
// 也会以初始值为参数调用一次
},
unbind: function () {
// 清理工作
// 例如, 删除 bind() 添加的事件监听器
}
})
<div v-my-directive="someValue"></div>
new Vue({
data: {
firstName: 'Foo'
},
watch: {
firstName: function (val, oldVal) {}
}
})
{{ msg | capitalize }} // 'abc' => 'Abc'
常见内置过滤器
capitalize
, uppercase
, lowercase
, json
, limitBy
, filterBy
。所有见这里。
Vue2 中把这些内置的过滤器都删除了。
Vue.filter('wrap', function (value, begin, end) {
return begin + value + end
})
<!-- 'hello' => 'before hello after' -->
<!-- Vue1 这么写 -->
<span v-text="message | wrap 'before' 'after'"></span>
<!-- Vue2 这么写 -->
<span v-text="message | wrap('before', 'after')"></span>
this.$options.filters.filter名称
可以获取到具体的 filter
// Vue1
new Vue({
created: function () {},
beforeCompile: function () {},
compiled: function () {},
ready: function () {}, // DOM 元素已经加入到HTML中(可以在这里调用ajax获取数据)
beforeDestroy: function () {},
destroyed: function () {}
})
// Vue2
new Vue({
created: function () {},
mounted: function () {}, 相对于 Vue1 中的 ready
beforeDestroy: function () {},
destroyed: function () {}
})
<!-- Vue1 这么写 -->
<div v-if="show" transition="my-transition"></div>
<!-- Vue2 这么写 -->
<transition v-bind:name="my-transition">
<!-- ... -->
</transition>
/* 必须 */
.my-transition-transition {
transition: all .3s ease;
}
/* .my-transition-enter 定义进入的开始状态 */
.my-transition-enter {}
/* .my-transition-leave 定义离开的结束状态 */
.my-transition-leave {}
Vue2 与 Vue1 的组件区别有点大。
this.$parent
访问它的父组件。
this.$root
访问它的根组件。
this.$children
访问它的子组件。
用于解决 不能检测到属性添加、属性删除的限制。
// 修改数据
vm.msg = 'Hello'
// DOM 没有更新
Vue.nextTick(function () {
// DOM 更新了
})
Vue 在检测到数据变化时是异步更新 DOM 的。vm 上也有 this.$nextTick
。
Vue.http.interceptors.push(function(request, next) {
var data = request.data;
// 添加 url 前缀
request.url = serverPrefix + request.url;
// 加请求参数
request.data.sessionid = localStorage.getItem('sessionid');
next(function (response) {
if(登陆超时){
setTimeout(function () {
router.go('/login');
});
} else {
// modify response
response.body = '...';
}
});
});
Vue.http.post('/someUrl', [optinos])
.then(function(res) {
var data = res.data;
return new Promise(function(resolve, reject) {
if (!data.error) {
reject()
} else {
resolve(data);
}
}.bind(this));
})
.then(function(res) {
var data = res.data;
return new Promise(function(resolve, reject) {
Vue.http.post('/someUrl', data).then(function (res) {
if(res.data){
resolve();
}
});
}.bind(this));
}, failFn)
.then(succFn, failFn);
absolute
)
div {
position: absolute;
width: 300px;
height: 300px;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
relative/absolute
)确定容器的宽高 宽500 高300 的层
设置层的外边距
div {
position: relative; /* 相对定位或绝对定位均可 */
width:500px;
height:300px;
top: 50%;
left: 50%;
margin: -150px 0 0 -250px; /* 外边距为自身宽高的一半 */
background-color: pink; /* 方便看效果 */
}
transform
)未知容器的宽高,利用 transform
属性
div {
position: absolute; /* 相对定位或绝对定位均可 */
width:500px;
height:300px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: pink; /* 方便看效果 */
}
flex
)
.container {
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
}
.container div {
width: 100px;
height: 100px;
background-color: pink; /* 方便看效果 */
}
~
/ +
兄弟选择器来美化表达元素**选择器解析**
- `~` 选择器:查找某一个元素的后面的所有兄弟元素
- `+` 选择器:查找某一个元素的后面紧邻的兄弟元素
switch 开关
radio 美化
demo:
<div class="container goods-info">
<div class="row goods-tags">
<div class="col-md-2 tag-label">选择版本</div>
<div class="col-md-10">
<div class="tags-select">
<label class="tag-select">
<input type="radio" name="version" value="1">
<span class="name">全网通(2GB 16GB)</span>
</label>
<label class="tag-select">
<input type="radio" name="version" value="2">
<span class="name">全网通(3GB 32GB)</span>
</label>
<label class="tag-select">
<input type="radio" name="version" value="3" disabled>
<span class="name">联通版(2GB 16GB)</span>
</label>
</div>
</div>
</div>
<div class="row goods-tags">
<div class="col-md-2 tag-label">购买方式</div>
<div class="col-md-10">
<div class="tags-select">
<label class="tag-select">
<input type="radio" name="bye-type" value="1">
<span class="name">官方标配</span>
</label>
<label class="tag-select">
<input type="radio" name="bye-type" value="2">
<span class="name">移动优惠购</span>
</label>
<label class="tag-select">
<input type="radio" name="bye-type" value="3" disabled>
<span class="name">联通优惠购</span>
</label>
<label class="tag-select">
<input type="radio" name="bye-type" value="4">
<span class="name">电信优惠购</span>
</label>
</div>
</div>
</div>
</div>
.goods-info {
margin-top: 50px;
margin-bottom: 50px;
.tag-label {
padding-top: 7px;
}
.goods-tags {
margin-top: 2rem;
}
}
.tags-select {
font-size: 0;
> .tag-select {
display: inline-block;
font-size: 14px;
margin: 5px;
position: relative;
font-weight: normal;
.name {
display: block;
line-height: 20px;
padding: 8px 10px;
border: 1px solid #ccc;
cursor: pointer;
}
input[type='radio'] {
position: absolute;
opacity: 0;
z-index: -1;
&:checked + .name {
border-color: #e3393c;
}
&:disabled + .name {
background: #eee;
color: #999;
cursor: not-allowed;
}
}
}
}
font-size: 0
来清除间距inline-block
的元素之间会受空白区域的影响,也就是元素之间差不多会有一个字符的空隙。你可以利用元素浮动float
, 或者压缩html、清除元素间的空格来解决。但最简单有效的方法还是设父元素的 font-size
属性为 0
。
demo:
* {
box-sizing: border-box;
}
.items {
font-size: 0;
> .item {
display: inline-block;
width: 25%;
height: 50px;
border: 1px solid #ccc;
text-align: center;
line-height: 50px;
background-color: #eee;
font-size: 16px; // 不要忘了给子元素设置字号
}
}
<div class="items">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
boder
来绘制三角形**原理:**
只需要将其他三个边的颜色设置为透明(`transparent` 或者 `rgba(0, 0, 0, 0)`),就会只保留一个三角形了。
## 用垂直方向的 `padding` 来实现等比缩放的盒子
固定图片百分比是一个针对响应式布局很有效的方案。简单来说,就是根据容器的宽度,按照宽高比例自动计算出容器的大小,并且让容器内的如 `img` 等子元素自适应宽高。
假设图片的比例是 4:3:
图片父容器宽度 100%,父容器的高度百分比为:`100 * 3 / 4 = 75%`;图片 `absolute` 并且完全铺满父容器。
```scss
.image-aspect-ratio {
width: 100%;
position: relative;
padding-top: 75%;
> img {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
}
<figure class="image-aspect-ratio">
<img src="http://via.placeholder.com/640x384">
</figure>
pointer-event
来禁用事件pointer-event
属性更像是一个 JavaScript 事件,利用该属性,可以做以下事情:
cursor: default
)hover
和 active
状态// 使用该类,任何点击事件将无效
.disabled { pointer-events: none;}
max-width
来防止图片撑破容器img {
display: inline-block;
max-width: 100%;
}
在已知父子高度的情况下,实现垂直居中是很容易的事。margin
、padding
、absolute + 负margin
,甚至 line-height
都是可行的方案。
这里主要介绍在父容器高度固定,子容器高度自适应的情况下,来实现其垂直居中于父盒子的几种方案。
html结构:
<div class="container-fluid">
<div class="row">
<!-- :before -->
<div class="col-md-6">
<div class="vh-modal vh-modal-1">
<div class="vh-modal-content">
<h3 class="vh-modal-title">模态框</h3>
<div class="vh-modal-body">
<h4>(伪)元素占位</h4>
<p>使用(伪)元素占位法可以实现子元素在父元素上的垂直居中,然后利用父元素设置
<code>text-align: center</code>实现水平居中,这是一种有效的实现水平垂直居中的方案</p>
</div>
<div class="vh-modal-foot">
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
<!-- transform -->
<div class="col-md-6">
<div class="vh-modal vh-modal-2">
<div class="vh-modal-content">
<h3 class="vh-modal-title">模态框</h3>
<div class="vh-modal-body">
<h4>absolute + transform</h4>
<p>
<span>利用
<code>absolute</code>绝对定位和css3的
<code>transform</code>位移属性配合,也可以实现子元素在父元素上的水平垂直居中。但是,如果子元素已经在高度上超过视窗高度,那它的顶部会被浏览器窗口裁掉。</span>
</p>
</div>
<div class="vh-modal-foot">
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- table -->
<div class="col-md-12">
<div class="vh-modal vh-modal-3">
<div class="vh-modal-cell">
<div class="vh-modal-content">
<h3 class="vh-modal-title">模态框</h3>
<div class="vh-modal-body">
<h4>table-cell</h4>
<p>
利用div模拟table属性,也是一种很实用的实现子元素垂直居中的方法,确定是table的不可控属性以及要给子元素再包裹一层父元素
</p>
</div>
<div class="vh-modal-foot">
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- flex -->
<div class="col-md-6">
<div class="vh-modal vh-modal-4">
<div class="vh-modal-content">
<h3 class="vh-modal-title">模态框</h3>
<div class="vh-modal-body">
<h4>flex</h4>
<p>
在移动端中优先推荐
<code>flex</code>方案,目前,除了部分机型的UC浏览器,
<code>flex</code>在手机浏览器中已经得到了很好的支持。
</p>
</div>
<div class="vh-modal-foot">
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
<!-- flex+margin:auto -->
<div class="col-md-6">
<div class="vh-modal vh-modal-5">
<div class="vh-modal-content">
<h3 class="vh-modal-title">模态框</h3>
<div class="vh-modal-body">
<h4>flex+margin:auto</h4>
<p>
当子元素处于
<code>flex</code>盒模型中,仅仅给子元素设置一个
<code>margin:auto</code>的属性也完全可以实现水平和垂直方向的垂直居中,很神奇对吧?
</p>
</div>
<div class="vh-modal-foot">
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
</div>
</div>
</div>
推荐
)display: inline-block
的方案来实现垂直居中。.vh-modal-1 {
text-align: center;
font-size: 0;
&::before,
> .vh-modal-content {
display: inline-block;
vertical-align: middle;
font-size: 14px;
}
&::before {
content: '';
height: 100%;
}
}
如上图中的 ::before
你也可以使用一个真实的元素代替。
使用 absolute
决定定位子元素,并且设置其 top:50%;left:50%
,然后再利用 css3
的 transform: translate(-50%, -50%);
设置负值偏移回来也是一种有效的垂直居中方案,但是要注意其兼容性以及不要将子元素置于父容器半个像素的位置上(如 500.5px),否则子容器会出现模糊。
.vh-modal-2 {
> .vh-modal-content {
// 尽可能的不要让该元素的宽度出现奇数,否则可能会导致模糊
display: inline-block; // 为了自适应宽度,也可以固定宽度
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
transform
法还有一个缺点,就是当子元素容器高度超出视窗高度的时候,它会被直接截断(如下图),而不是想象中的随着浏览器滚动到顶部而滚动显示完全(模态框的头部被截掉了)。
table
的行为也可以实现垂直居中。缺点是要在子容器外层再包裹一个父元素vh-modal-cell
用来模拟table-cell
。// table-cell
.vh-modal-3 {
display: table;
width: 100%;
.vh-modal-cell {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.vh-modal-content {
display: inline-block;
}
}
<div class="vh-modal vh-modal-3">
<div class="vh-modal-cell">
<div class="vh-modal-content">
...
</div>
</div>
</div>
<!--
模拟了table布局
<table style="width: 100%;">
<tr>
<td style="text-align: center; vertical-align: middle;">
类似于直接使用table布局
</td>
</tr>
</table>
-->
强烈推荐
毫无疑问,flex
盒模型是最佳的实践方案。目前几乎所有现代浏览器都支持 flex
布局,尤其是移动端(部分机型UC浏览器效果太差)。基于 flex
盒模型的水平垂直居中有如下两种方案:
<div class="vh-modal vh-modal-4(5)">
<div class="vh-modal-content">
...
</div>
</div>
align-items & justify-content 方案
.vh-modal-4 {
display: flex;
align-items: center;
justify-content: center;
> .vh-modal-content {
}
}
flex + margin 方案
这个方案是最神奇的,仅仅给子元素设置了 margin: auto;
属性,一切就这么发生了。
.vh-modal-5 {
display: flex;
margin: 0;
> .vh-modal-content {
margin: auto;
}
}
counter
来模拟/装饰有序清单类似于截图中这种多层级的数字,我们大概第一反应就是使用 JavaScript
循环列表,利用其 index
拼接而成的。事实上,仅仅使用 css 的 counter
属性也可以实现该功能,甚至实现起来更高效。
ol {
counter-reset: decimal; /* 为每个ol元素创建新的计数器实例, 重置计数器成0 */
list-style-type: none; // 去掉默认的list-style
margin-left: 2rem;
li {
&::before {
counter-increment: decimal; /* 只增加计数器的当前实例 */
content: counters(decimal, '.') ' '; /* 显示计数器,并且为所有计数器实例增加以"."分隔的值 */
}
}
}
CSS计数器是CSS2.1中自动计数编号部分的实现。作为由CSS维护的变量,counter属性还有很多有趣的使用场景,具体就不展开了。请参考MDN上的 使用CSS计数器章节。
table-layout
来控制表格单元格宽度你也许遇到过给表格设置了宽度,但是不起作用的问题。这是因为单元格的宽度是根据其内容进行调整的。刨根到底,是因为表格有个叫做 table-layout
的属性,其浏览器默认值是 auto
在作怪。当我们把这个值设置为 fixed
的时候,我们给 th/td
标签设置的宽度就起作用了。用法很简单:
table {
table-layout: fixed;
width: 100%;
}
![](https://img.smohan.net/9563fb79d019dcbc0e81875ca258c8ad.png?imageView2/1/interlace/1/q/100)
截图是设置table-layout: fixed;前后对比图,左边用蓝色标注的是默认行为的表格,右边是设置了table-layout: fixed;后的样式。显而易见的,默认情况下,单元格宽度受其内容约束。而设置了table-layout: fixed;后,其单元格宽度变得可控了。
.table {
width: 100%;
&.fixed {
table-layout: fixed;
}
td.overflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
<div class="container-fluid">
<!-- row -->
<div class="row">
<div class="col-md-6">
<table class="table">
<thead>
<tr>
<th width="10%">#</th>
<th width="20%">用户</th>
<th width="70%">内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td class="overflow">[email protected]@[email protected]</td>
<td>通过为表格中的一行或一个单元格添加颜色而赋予不同的意义只是提供了一种视觉上的表现,并不能为使用辅助技术 -- 例如屏幕阅读器 -- 浏览网页的用户提供更多信息。因此,请确保通过颜色而赋予的不同意义可以通过内容本身来表达(即在相应行或单元格中的可见的文本内容);或者通过包含额外的方式
-- 例如应用了 .sr-only 类而隐藏的文本 -- 来表达出来</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table fixed">
<thead>
<tr>
<th width="10%">#</th>
<th width="20%">用户</th>
<th width="70%">内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td class="overflow">[email protected]@[email protected]</td>
<td>通过为表格中的一行或一个单元格添加颜色而赋予不同的意义只是提供了一种视觉上的表现,并不能为使用辅助技术 -- 例如屏幕阅读器 -- 浏览网页的用户提供更多信息。因此,请确保通过颜色而赋予的不同意义可以通过内容本身来表达(即在相应行或单元格中的可见的文本内容);或者通过包含额外的方式
-- 例如应用了 .sr-only 类而隐藏的文本 -- 来表达出来</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
caret-color
来定义光标的样式在文本框中 input/textarea
中如果要改变光标的颜色,可以通过设置文本的颜色 color: #f00
来搞定。但是假如我们只想改变光标的颜色,而不想改变文本的颜色的话,caret-color
属性是屎一个实现方案。
<input type="text" value="I have a custom caret color!">
input {
padding: 20px;
font-size: 18px;
width: 100%;
display: block;
}
input,
textarea,
[contenteditable] {
caret-color: red;
}
user-select
来禁用文本选中在现在浏览器中,只需要一句 user-select: none
的css样式就可以解决。IE6-9不支持
该属性,可以通过给 body
添加 onselectstart="return false;"
的内联 JavaScript
语句搞定。
body {
user-select: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>LazyLoad</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
img {
display: block;
margin-bottom: 50px;
width: 400px;
height: 500px;
}
</style>
</head>
<body>
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-RkZCZmhNZldvRy1NYTViWHFyYjZSZl9NUE5pM0hTN0pLSmZGSko3N3JUTXVfakNBMFVBbVVTaW9FVDhPOGNJdw.jpg" alt="">
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-ZHk5Y1ZCdHB6SWU4VDdhdWRsTzgxSmFCaThmZUJYQjRScngwd3N6M0NPY0h1ZFo0TzhoZnR0enpDRUVOTEdKWQ.jpg" alt="">
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-VjJKN2c3WDRZQXJTM2tabjkxM29yRzQxMkRSUjdFZV9OWHJVT2xueXlBeUY0VU1sZ1lUbFZscUwtLWtSVms1Uw.jpg" alt="">
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-TW82T2hHdk9iMlM4WGNadjU5bE0yNmRSTGZ6ZVlCNVdWWThORmR4eXJoaWdnel9tanAyWXBVemxZQWpDVjZydw.jpg" alt="">
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-aTQtNE5hVzVZRDFLZTFkeWVZNWRSbkZFXzhaMDhTblRFSXBieklXUUQ0LUhKVUxkNTZlZ3ZiTmVuRkRVVHRFNg.jpg" alt="">
<img src="images/default.jpg" data-src="http://lofter.nos.netease.com/sogou-eXFRQzQ4Tm92OEQ1VVl4b0pkWWdTQXFLUmJDS3BSX2dWR1VJT3pVZlZnM2w3YV9zS3lkT2VVZUVQckI4dDNCYg.jpg" alt="">
<script>
let images = document.getElementsByTagName('img')
let num = images.length
let n = 0 // 存储图片加载到的位置, 避免每次都从第一张图片开始遍历
function lazyload() {
console.log('lazyload...')
const seeHeight = document.documentElement.clientHeight // 可视区域高度
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop // 滚动条距离顶部高度
for (let i = n; i < num; i++) {
if (images[i].offsetTop < seeHeight + scrollTop) {
if (images[i].getAttribute('src') === 'images/default.jpg') {
images[i].src = images[i].getAttribute('data-src')
}
n = i + 1
}
}
}
/**
* 节流函数
* fun 要执行的函数
* delay 延迟时间
* time 在 time 时间内必须执行一次
*/
function throttle(fun, delay, time) {
let timeout = null
let startTime = new Date()
return function() {
var context = this
var args = arguments
var curTime = new Date()
clearTimeout(timeout)
// 如果达到了规定的触发时间间隔, 触发 handler
if (curTime - startTime >= time) {
fun.apply(context, args)
startTime = curTime // 还没达到触发间隔, 重新设定定时器
} else {
timeout = setTimeout(function() {
fun.apply(context, args)
}, delay)
}
}
}
lazyload() // 页面载入完毕初始化首页(可视区域)的页面图片
window.addEventListener('scroll', throttle(lazyload, 500, 1000), false)
</script>
</body>
</html>
loader
配置在module中,在使用 loader 时,可以通过 test
、include
、exclude
三个配置来命中 loader 要应用规则的文件。
以采用 ES6 的项目为例,在配置 babel-loader 时,可以这样:
module.exports = {
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
use: ['babel-loader?cacheDirectory'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
]
},
};
resolve.modules
的配置resolve.modules
用于配置 webpack 去哪些目录下寻找第三方模块。
resolve.modules
的默认目录是 [node_modules]
,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。
当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
module.exports = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')]
},
};
resolve.mainFields
配置resolve.mainFields
用于配置第三方模块使用哪个入口文件。
安装的第三方模块中都会有一个 package.json
文件用于描述这个模块的属性,其中有些字段用于描述入口文件在哪里, resolve.mainFields
用于配置采用哪个字段作为入口文件的描述。
可以存在多个字段描述入口文件的原因是因为有些模块可以同时用在多个环境,针对不同的运行环境需要不同的代码。 以 isomorphic-fetch 为例,它是 fetch API 的一个实现,但可同时用于浏览器和 Node.js 环境。 它的 package.json 中就有2个入口文件描述字段:
{
"browser": "fetch-npm-browserify.js",
"main": "fetch-npm-node.js"
}
resolve.mainFields
的默认值和当前的 target
配置有关系,对于关系如下:
target
为 web
或者 webworker
时,值是 ['browser', 'module', 'main']
target
为其他情况时,值是 ['module', 'main']
以 target
等于 web 为例,Webpack 会先采用第三方模块中的 browser
字段去寻找模块的入口文件,如果不存在就采用 module
字段,以此类推。
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少。 由于大多数第三方模块都采用 main 字段去描述入口文件的位置,可以这样配置 Webpack:
module.exports = {
resolve: {
// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['main'],
},
};
使用本方法优化时,你需要考虑到所有运行时依赖的第三方模块的入口文件描述字段,就算有一个模块搞错了都可能会造成构建出的代码无法正常运行。
resolve.alias
配置resolve.alias
配置项通过别名来把原导入路径映射成一个新的导入路径。
在实战项目中,经常会依赖一些庞大的第三方模块,以React库为例,安装到 node_modules
目录下的 React 库的目录结构如下:
├── dist
│ ├── react.js
│ └── react.min.js
├── lib
│ ... 还有几十个文件被忽略
│ ├── LinkedStateMixin.js
│ ├── createClass.js
│ └── React.js
├── package.json
└── react.js
可以看到发布出去的 React 库中包含两套代码:
lib
目录下,以 package.json
中指定的入口文件 react.js
为模块的入口。dist/react.js
是用于开发环境,里面包含检查和警告的代码。dist/react.min.js
是用于线上环境,被最小化了。./node_modules/react/react.js
开始递归的解析和处理依赖的几十个文件,这会时一个耗时的操作。 通过配置 resolve.alias
可以让 Webpack 在处理 React 库时,直接使用单独完整的 react.min.js
文件,从而跳过耗时的递归解析操作。相关 webpack 配置如下:
module.exports = {
resolve: {
// 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
// 减少耗时的递归解析操作
alias: {
'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
}
},
};
除了 React 库外,大多数库发布到 Npm 仓库中时都会包含打包好的完整文件,对于这些库你也可以对它们配置 alias。
但是对于有些库使用本优化方法后会影响到后面要讲的使用 Tree-Shaking 去除无效代码的优化,因为打包好的完整文件中有部分代码你的项目可能永远用不上。 一般对整体性比较强的库采用本方法优化,因为完整文件中的代码是一个整体,每一行都是不可或缺的。 但是对于一些工具类的库,例如 lodash,你的项目可能只用到了其中几个工具函数,你就不能使用本方法去优化,因为这会导致你的输出代码中包含很多永远不会执行的代码。
resolve.extensions
配置在导入语句没有带文件后缀时,webpack会自动带上后缀去尝试询问文件是否存在。 resolve.extensions
用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']
也就是说当遇到 require('./data')
这样的导入语句时,Webpack 会先去寻找 ./data.js
文件,如果该文件不存在就去寻找 ./data.json
文件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions
的配置也会影响到构建的性能。 在配置 resolve.extensions
时你需要遵守以下几点,以做到尽可能的优化构建性能:
require('./data')写成require('./data.json')
相关 webpack 配置如下:
module.exports = {
resolve: {
// 尽可能的减少后缀尝试的可能性
extensions: ['js'],
},
};
module.noParse
配置module.noParse
配置项可以让 webpack 忽略对部分没有采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库,例如 jQuery、ChartJS,它们庞大又没有采用模块化标准,让 webpack 去解析这些文件耗时又没有意义。
在上面的优化 resolve.alias
配置中讲到单独完整的 react.min.js
文件就没有采用模块化,让我们来通过配置 module.noParse
忽略对 react.min.js
文件的递归解析处理,相关的webpack配置如下:
const path = require('path');
module.exports = {
module: {
// 独完整的 `react.min.js` 文件就没有采用模块化,忽略对 `react.min.js` 文件的递归解析处理
noParse: [/react\.min\.js$/],
},
};
注意被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中包含无法在浏览器环境下执行的模块化语句。
要给 Web 项目构建接入动态链接库的**,需要完成以下事情:
为什么给 Web 项目构建接入动态链接库的**后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会在重新编译,而是直接使用动态链接库中的代码。 由于动态链接库中大多数包含的是常用的第三方模块,例如 react
、react-dom
,只要不升级这些模块的版本,动态链接库就不用重新编译。
webpack 已经内置了对动态链接库的支持,需要通过2个内置的插件接入,它们分别是:
下面以基本的 React 项目为例,与其接入 DllPlugin, 在开始前先来看下最终构建出的目录结构:
├── main.js
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
其中包含两个动态链接库文件,分别是:
polyfill.dll.js
里面包含项目所有依赖的 polyfill
,例如 Promise、fetch 等 API。react.dll.js
里面包含 React 的基础运行环境,也就是 react
和 react-dom
模块。react.dll.js
文件为例,其文件内容大致如下:var _dll_react = (function(modules) {
// ... 此处省略 webpackBootstrap 函数代码
}([
function(module, exports, __webpack_require__) {
// 模块 ID 为 0 的模块对应的代码
},
function(module, exports, __webpack_require__) {
// 模块 ID 为 1 的模块对应的代码
},
// ... 此处省略剩下的模块对应的代码
]));
可见一个动态链接库文件中包含了大量模块的代码,这些模块存放在一个数组里,用数组的索引号作为 ID。 并且还通过 _dll_react
变量把自己暴露在了全局中,也就是可以通过 window._dll_react
可以访问到它里面包含的模块。
其中 polyfill.manifest.json
和 react.manifest.json
文件也是由 DllPlugin 生成出,用于描述动态链接库文件中包含哪些模块, 以 react.manifest.json
文件为例,其文件内容大致如下:
{
// 描述该动态链接库文件暴露在全局的变量名称
"name": "_dll_react",
"content": {
"./node_modules/process/browser.js": {
"id": 0,
"meta": {}
},
// ... 此处省略部分模块
"./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
"id": 42,
"meta": {}
},
"./node_modules/react/lib/lowPriorityWarning.js": {
"id": 47,
"meta": {}
},
// ... 此处省略部分模块
"./node_modules/react-dom/lib/SyntheticTouchEvent.js": {
"id": 210,
"meta": {}
},
"./node_modules/react-dom/lib/SyntheticTransitionEvent.js": {
"id": 211,
"meta": {}
},
}
}
可见 manifest.json
文件清楚地描述了与其对应的 dll.js
文件中包含了哪些模块,以及每个模块的路径和 ID。
main.js
文件是编译出来的执行入口文件,当遇到其依赖的模块在 dll.js
文件中时,会直接通过 dll.js
文件暴露出的全局变量去获取打包在 dll.js
文件的模块。 所以在 index.html
文件中需要把依赖的两个 dll.js
文件给加载进去,index.html
内容如下:
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入依赖的动态链接库文件-->
<script src="./dist/polyfill.dll.js"></script>
<script src="./dist/react.dll.js"></script>
<!--导入执行入口文件-->
<script src="./dist/main.js"></script>
</body>
</html>
以上就是所有接入 DllPlugin 后最终编译出来的代码,接下来教你如何实现。
构建输出的以下这四个文件:
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
和以下这一个文件:
├── main.js
是由两份不同的构建分别输出的。
动态链接库文件相关的文件需要由一份独立的构建输出,用于给主构建使用。新建一个 webpack 配置文件 webpack_dll.config.js
专门用于构建它们,文件内容如下:
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
// JS 执行入口文件
entry: {
// 把 React 相关模块放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch']
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是entry中配置的 react 和 polyfill
filename: '[name].dll.js',
// 存放的文件都存放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_是为了防止全局变量冲突
library: '_dll_[name]'
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件中 name 字段的值
// 例如 react.mainfest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 mainfest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].mainfest.json')
})
]
}
构建出的动态链接库文件用于给其他地方使用,在这里也就是给执行使用。
用于输出 mian.js
的主 webpack 配置文件内容如下:
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry: {
// 定义入口 Chunk
main: './main.js'
},
output: {
// 输出文件的名称
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
devtool: 'source-map'
};
注意:
在 webpack_dll.config.js
文件中,plugins DllPlugin 中的name
参数必须和output.library
中保持一致。原因在于 DllPlugin 中的name
参数会影响输出的mainfest.json
文件中的name
字段的值,而在webpack.config.js
文件中 DllReferencePlugin 会去mainfest.json
文件读取name
字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名。
在修改好以上两个 webpack 配置文件后,需要重新执行构建。重新执行构建时要注意的是需要先把动态链接库相关的文件编译出来,因为主 webpack 配置文件中定义的 DllReferencePlugin 依赖这些文件。
执行构建时流程如下:
如果动态链接库相关的文件还没有编译出来,就需要先把它们编译出来。方法是执行webpack --config webpack_dll.config.js
命令。
在确保动态链接库存在时,才能正常的编译出入口执行文件。方法是执行 webpack 命令。
接入 HappyPack 的相关代码如下:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// ... 其它配置项
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};
以上代码有两点重要的修改:
happypack/loader
去处理,使用紧跟其后的 querystring ?id=babel
去告诉 happypack/loader
去选择哪个 HappyPack 实例去处理文件。happypack/loader
去如何处理 .js
和 .css
文件。选项中的 id
属性的值和上面 querystring
中的 ?id=babel
相对应,选项中的 loaders
属性和 Loader 配置中一样。在实例化 HappyPack 插件的时候,除了可以传入 id
和 loaders
两个参数外, HappyPack 还支持如下参数:
threads
代表开启几个子进程去处理这一类型的文件,默认是 3
个,类型必须是整数。verbose
是否允许 HappyPack 输出日志, 默认是 true
。threadPool
代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多,相关代码如下:const HappyPack = require('happypack');
// 构造出共享进程池,进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });
module.exports = {
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};
ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行
使用 ParallelUglifyPlugin 也非常简单,把原来 Webpack 配置文件中内置的 UglifyJsPlugin 去掉后,再替换成 ParallelUglifyPlugin,相关代码如下:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};
侧重优化开发体验的配置文件 webpack.config.js:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
// HTML 模版文件所在的文件路径
template: './template.html',
// 提取出所有页面公共的代码
commonsChunk: {
// 提取出公共代码 Chunk 的名称
name: 'common',
},
// 指定存放 CSS 文件的 CDN 目录 URL
stylePublicPath: '//css.cdn.com/id/',
});
module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './src/base.js',
}),
output: {
// 给输出的文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目录 URL
publicPath: '//js.cdn.com/id/',
},
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['jsnext:main', 'main'],
},
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// 使用 HappyPack 加速构建
use: ['happypack/loader?id=babel'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
{
test: /\.js$/,
use: ['happypack/loader?id=ui-component'],
include: path.resolve(__dirname, 'src'),
},
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
// 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
publicPath: '//img.cdn.com/id/'
}),
},
]
},
plugins: [
autoWebPlugin,
// 4-14开启ScopeHoisting
new ModuleConcatenationPlugin(),
// 4-3使用HappyPack
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
// UI 组件加载拆分
id: 'ui-component',
loaders: [{
loader: 'ui-component-loader',
options: {
lib: 'antd',
style: 'style/index.css',
camel2: '-'
}
}],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
// 通过 minimize 选项压缩 CSS 代码
loaders: ['css-loader?minimize'],
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 4-11提取公共代码
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
]
};
它之所以能实现等高布局,跟一个flex的css属性有关系,这个属性是:align-item
。它的默认值是: stretch
, 在flex item元素比如layout__main或layout__aside的高度未定义或者为auto的情况下,会拉伸flex item元素的高度或宽度,铺满flex的交叉轴。
2)当把一个元素的display属性设置成以上列出的值后,就可以把这个元素看成与该属性对应的表格元素,比如 table-cell对应的就是td;同时这个元素会拥有跟表格一样的特性,比如display:table或者inline-table的元素可以使用table-layout,border-collapse和border-spacing这三个原本只有table才能生效的属性;display:table-cell
的元素跟 td
一样,对宽高敏感,对margin值无反应,对padding有效。
3)关于table-cell还有一点要说明的就是,它会被其他一些CSS属性破坏,例如 float, position: absolute, 所以这些属性不能同时使用。
4)跟直接使用表格元素不同的是,在使用表格元素的时候需要完全遵守表格元素嵌套结构,也就是下面这种:
<table>
<thead>
<th></th>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
<tfoot>
<th></th>
</tfoot>
</table>
而使用伪table的哪些属性时,可以近单独使用某一个属性,浏览器会在这些元素的外层包裹缺失的框来保证伪table元素框嵌套结构的完整性,这些框跟常提到过的行框一样都是不可见的,网上有的文章里也把这种做法叫做匿名表格。下面这个代码中,tb-cell元素的外层没有加 display: table-row和display: table的元素:
.tb-cell {
display: table-cell;
padding: 10px;
border: 1px solid #ccc;
}
<div class="tb-cell">这是第1个display: table-cell;的元素。</div>
<div class="tb-cell">这是第2个display: table-cell;的元素。</div>
但是看到的效果是(蓝色背景是它们父层的一个包裹元素: width: 800px;margin-left: auto;margin-right: auto):
因为浏览器自动在这两个元素的外层,加了跟能够跟tr和table起相同作用的框,来包含这两个元素形成的框,所以这两个元素看起来就跟实际的表格效果一样。假如浏览器没有做这个处理,这两个元素之间是不可能没有间隙的,中间会有一个因为换行符显示出来的空格。这种自动添加的框都是行内框,不是块级框。
接下来看看如何通过这些伪table的属性来完成上文的分栏布局以及本文要求的等高分栏布局,玩法有很多:(本文相关源码下载)
这种方法的思路是布局时完全按照表格的嵌套层次来处理,把display: table, display: table-row, display: table-cell 都用上,相当于就是利用完整的table来做,比如说要实现上文的布局(3栏布局,2个侧边栏布局分别固定在左边和右边,中间是主体内容栏),就可以这么干:
<style type="text/css">
.layout {
display: table;
width: 100%;
}
.layout__row {
display: table-row;
}
.layout__col {
text-align: center;
display: table-cell;
}
.layout__col + .layout__col {
border-left: 10px solid #fff;
}
.layout__main {
background-color: #4DBCB0;
}
.layout__aside {
width: 200px;
background-color: #daf1ef;
}
</style>
<div class="layout">
<div class="layout__row">
<aside class="layout__col layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__col layout__main">内容栏宽度自适应<br>高度增加一点,旁边的高度都会自动增加</div>
<aside class="layout__col layout__aside layout__aside--right">右侧边栏宽度固定</aside>
</div>
</div>
缺点是分栏之间的间隔不能用margin和padding来做,如果用margin,这个属性在display: table-cell的元素上根本不会生效;如果用padding,那像demo里面各栏的背景色就都会连到一块,做不出间隔的效果,如果在layout__col里面再嵌套一层,在这一层设置背景色的话,又会增加html的层次,也不是很好。我这里是投了个巧,用border处理了一下
前面说过,浏览器会用匿名表格的方式,添加缺失的框,所以玩法一中的代码,把layout-row完全去掉,一点都不影响布局效果:
<style type="text/css">
.layout {
display: table;
width: 100%;
}
.layout__col {
text-align: center;
display: table-cell;
}
.layout__col + .layout__col {
border-left: 10px solid #fff;
}
.layout__main {
background-color: #4DBCB0;
}
.layout__aside {
width: 200px;
background-color: #daf1ef;
}
</style>
<div class="layout">
<aside class="layout__col layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__col layout__main">内容栏宽度自适应<br>高度增加一点,旁边的高度都会自动增加</div>
<aside class="layout__col layout__aside layout__aside--right">右侧边栏宽度固定</aside>
</div>
根据玩法二,可以试想一下能否再把display:table这一个属性去掉,反正浏览器还会在天津框来包裹:
<style type="text/css">
.layout__col {
text-align: center;
display: table-cell;
}
.layout__col + .layout__col {
border-left: 10px solid #fff;
}
.layout__main {
background-color: #4DBCB0;
}
.layout__aside {
width: 200px;
min-width: 200px;
background-color: #daf1ef;
}
</style>
<div class="layout">
<aside class="layout__col layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__col layout__main">内容栏宽度自适应<br>高度增加一点,旁边的高度都会自动增加</div>
<aside class="layout__col layout__aside layout__aside--right">右侧边栏宽度固定</aside>
</div>
如果网站比较简单,去掉layout这一层包裹元素也是可以的:
<header>顶部</header>
<aside class="layout__col layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__col layout__main">内容栏宽度自适应<br>高度增加一点,旁边的高度都会自动增加</div>
<aside class="layout__col layout__aside layout__aside--right">右侧边栏宽度固定</aside>
<footer>底部</footer>
<style type="text/css">
.layout__col {
text-align: center;
display: table-cell;
line-height: 50px;
}
.layout__col + .layout__col {
border-left: 10px solid #fff;
}
.layout__main {
width: 3000px;
background-color: #4DBCB0;
}
.layout__aside {
width: 200px;
min-width: 200px;
background-color: #daf1ef;
}
</style>
<header>顶部</header>
<div class="layout">
<aside class="layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__main">内容栏宽度自适应</div>
<aside class="layout__aside layout__aside--right">右侧边栏宽度固定</aside>
</div>
<footer>底部</footer>
<style type="text/css">
.layout {
height: 300px;
position: relative;
}
.layout__aside, .layout__main {
position: absolute;
top: 0;
bottom: 0;
}
.layout__main {
left: 210px;
right: 210px;
}
.layout__aside {
width: 200px;
}
.layout__aside--left {
left: 0;
}
.layout__aside--right {
right: 0;
}
</style>
这种布局方法的特点是:
1)主体内容栏是自适应的
2)所有栏完全等高,效果跟flex布局和伪table布局的效果一样
存在一个非常大的使用限制,就是父元素的高度没有办法通过它的内部元素给撑起来,要用的话,必须想办法让父元素有高度,适合做父元素高度可知或者全屏布局。
<div class="layout">
<aside class="layout__aside layout__aside--left">左侧边栏宽度固定</aside>
<div class="layout__main">内容栏宽度自适应<br>高度增加一点,旁边的高度都会自动增加</div>
<aside class="layout__aside layout__aside--right">右侧边栏宽度固定</aside>
</div>
<style type="text/css">
.layout {
position: relative;
}
.layout__aside {
position: absolute;
top: 0;
bottom: 0;
}
.layout__main {
margin: 0 210px;
}
.layout__aside {
width: 200px;
}
.layout__aside--left {
left: 0;
}
.layout__aside--right {
right: 0;
}
</style>
这个方法的特点是:
1)主体内容栏是宽度自适应的
2)所有栏也是完全等高的
在做业务开发的时候,碰到过这样的问题:
部分浏览器,如 Safari 中,从后一个页面返回到上一个页面的时候,页面并不会进行刷新,而是维持着该页面最后的状态。这样在用户不知道已经提交过的情况下,就有可能再次提交,导致用户体验不佳等情况
那么该如何解决这种问题呢?我们很容易会联想到如果可以像原生 App 开发中一样,监听到页面的生命周期,那这个问题自然就迎刃而解
那么浏览器中有没有页面的生命周期方法呢?其实并没有,但是却有类似功能的事件来帮助我们解决问题
浏览器的页面事件有下面几种:
下面对这些事件进行说明:
该事件在页面第一次加载时触发,如果页面是从浏览器缓存中读取的,则不会触发
window.onload = (e) => {
alert('page onload')
}
该事件在页面每次加载时都会触发,也就是说,即使页面是从浏览器中读取的,也会触发这个事件
机智的小伙伴一定想到了,我们可以用这个方法来判断返回后的页面是不是直接读取的缓存中的页面
但是还有一个问题,onpageshow
虽然每次都会触发,但是该如何判断是不是从缓存中读取的呢?请看下面的代码:
window.onpageshow = (e) => {
if (e.persisted) {
alert('page read from cache')
window.location.reload()
}
上面代码中,通过事件对象的属性 persisted
来判断是否从缓存中读取,它会返回 true
或 false
但是并不是所有浏览器都需要添加这个方法,现在很多浏览器在返回后都会自动刷新,所以我们只需要对那些不会刷新的浏览器的来使用这个方法方法就可以了
这两个事件都是离开页面时触发的事件,区别在于 onpagehide
触发后依然可以对页面进行缓存,而 onunload
触发后无法缓存页面
如果同时使用这两个方法的话,会发现 onpagehide
会先于 onunload
被触发,并且在这两个事件中都无法使用 alert
页面关闭之前触发的事件,先于上面的两个离开页面事件被触发,同样无法使用alert
onload -> onpageshow -> onbeforeunload -> onpagehide -> onunload
设计稿: iphone尺寸(750 * 1334 )
(function(doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange': 'resize',
recalc = function() {
var clientWidth = docEl.clientWidth || 320;
var width = (clientWidth <= 320) ? 320: ((clientWidth >= 750) ? 750: clientWidth);
var fontSize = 100 * (width / 375);
docEl.style.fontSize = fontSize + 'px'
};
if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
recalc()
})(document, window);
具体使用方法:
根元素(html)在iphone下为 100px
。
div 在设计稿上宽高为-> 60px * 44px, 转为rem单位的过程: 宽: (60 / 2 / 100)rem -> 0.3rem; 高: (40 / 2/ 100)rem。
CSS 中,一个 CSS 变量是任意一个以两个破折号开头
的“属性”值,
.block {
color: #8cacea;
--color: blue;
}
CSS 变量也称作“自定义属性”
CSS 变量都有一个作用域。
:root {
--main-color: red;
}
:root
选择器能让我们选择 DOM 树里最顶级的元素,也就是文档树。所有这样定义的变量,也就相当于全局变量了。
PS: 如何在局部和全局范围内定义变量
变量定义且被赋值后,你就可以使用它了。
需要使用 var()
这个函数引用变量。
:root {
--font-size: 20px;
}
.test {
font-size: var(--font-size);
}
CSS 的变量需要非常小心谨慎,如果需要你还能做数学运算。多数情况下你应该把它们当作属性来使用。
/* 下面这样是不对的 */
.margin {
--side: margin-top;
var(--side): 20px;
}
这样定义会抛出无效属性名的语法错误
你也无法直接使用数学运算功能。运算功能需要使用 calc()
函数。
.margin {
--space: 20px * 2;
font-size: var(--space); // 这不是 40px
}
如果你一定需要使用数学计算,那么请使用 calc()
函数,如下:
.margin {
--space: calc(20px * 2);
font-size: var(--space); /* 等于 40px */
}
有一些行为是值得提醒注意的。
在段落 p 元素,section,aside 元素或者 root 根元素,甚至是伪元素上使用变量,都是可以的。
它们和普通属性一样工作
div {
--color: red;
}
div.test {
color: var(--color);
}
div.ew {
color: var(--color);
}
和普通变量相同,--color
的值也会从其他的 div 元素继承下来。
@media
或其他条件选择的规则同时使用和其他属性一样,你也可以使用 @media
或其他的条件规则里使用变量。
例如,下面的代码改变 变量的值,在不同的设备上使用不同值。
:root {
--gutter: 10px;
}
@media screen and (min-width: 768px) {
-gutter: 30px;
}
你也可以在内联样式里使用变量,它们也正常工作。
<html style="--color: red;">
body {
color: var(--color);
}
PS: CSS 变量大小写敏感。
/* 这是两个不同的变量 */
:root {
--color: blue;
--COLOR: red;
}
和其他属性是一样,重定义变量也遵循标准级联规则。
下面来看个例子:
/* 变量定义 */
:root {
--color: blue;
}
div {
--color: green;
}
#alert {
--color: red;
}
/*使用变量*/
* {
color: var(--color);
}
有了上面的定义,不同的元素的值是什么样的呢?
<p>我的颜色是?</p>
<div>我的呢?</div>
<div id="alert">
我的颜色是?
<p>颜色?</p>
</div>
第一个 p 元素是 blue
, 没有任何 --color
变量定义是在 p
元素的,因此它会继承自根元素 :root
。
第一个 div
是绿色 green。在div
上有一个颜色变量定义。
div {
--color: green;
}
使用 ID 值为 alert
定义的 div
, 不是绿色,而是红色 red
。
#alert {
color: red;
}
循环依赖通常会在下面几种情况下发生:
1
如果想把数组里面的字符串为数字的话, 比较常用的是
map
和parseInt
这两个方法。(然而执行的结果却不是我们想要的结果)
var a = ["1", "2", "3", "4", "5"]
var b = a.map(parseInt)
console.log(b) // [1, NaN, NaN, NaN, NaN]
map
方法的 callback 函数有三个参数:currentValue
,index
,array
。
parseInt
方法却接收两个参数:currentValue
,index
, 并且用 index 作为 index进制
parseInt('1', 0) // OK => 1
parseInt('2', 1) // 不合法的进制(没有一进制的呀。。。)
parseInt('3', 2) // NaN, 二进制中没有 3
parseInt('4', 3) // NaN, 三进制中没有 4
parseInt('5', 4) // NaN, 四进制中没有 5
.map(parseFloat)
, 因为 parseFloat
只接收一个参数。var c = a.map(parseFloat)
.map(Number)
var d = a.map(Number)
.map(num => parseInt(num))
, 只传递 currentValue 给 parseInt。var e = a.map(num => parseInt(num))
面试官常常会问:“来谈谈浏览器兼容性的问题吧”,“你对浏览器的兼容性有了解过吗”。
虽然面试官的问题十分的笼统,浏览器的兼容性无非还是样式兼容性(css),交互兼容性(js),浏览器hack三个方面。
* {margin: 0; padding: 0;}
浏览器内核与前缀的对应关系如下:
内核 | 主要代表的浏览器 | 前缀 |
---|---|---|
Trident | IE浏览器 | -ms |
Gecko | Firefox | -moz |
Presto | Opera | -o |
Webkit | Chrome和Safari | -webkit |
opacity: 0.5;
filter: alpha(opacity = 50); //IE6-IE8我们习惯使用filter滤镜属性来进行实现
filter: progid:DXImageTransform.Microsoft.Alpha(style = 0, opacity = 50); //IE4-IE9都支持滤镜写法progid:DXImageTransform.Microsoft.Alpha(Opacity=xx)
事件兼容的问题,我们通常需要会封装一个适配器的方法,过滤事件句柄绑定、移除、冒泡阻止以及默认事件行为处理
var helper = {}
//绑定事件
helper.on = function(target, type, handler) {
if(target.addEventListener) {
target.addEventListener(type, handler, false);
} else {
target.attachEvent("on" + type,
function(event) {
return handler.call(target, event);
}, false);
}
};
//取消事件监听
helper.remove = function(target, type, handler) {
if(target.removeEventListener) {
target.removeEventListener(type, handler);
} else {
target.detachEvent("on" + type,
function(event) {
return handler.call(target, event);
}, true);
}
};
new Date()构造函数使用,'2018-07-05'是无法被各个浏览器中,使用new Date(str)来正确生成日期对象的。 正确的用法是'2018/07/05'.
获取 scrollTop 通过 document.documentElement.scrollTop 兼容非chrome浏览器
var scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
快速判断 IE 浏览器版本
<!--[if IE 8]> ie8 <![endif]-->
<!--[if IE 9]> *气的 ie9 浏览器 <![endif]-->
判断是否是 Safari 浏览器
/* Safari */
var isSafari = /a/.__proto__=='//';
判断是否是 Chrome 浏览器
/* Chrome */
var isChrome = Boolean(window.chrome);
entry
output
resolve
module
plugins
devServer
数据类型有两种:原始数据类型(又称基础数据类型、可变数据类型)和复杂数据类型(又称对象、不可变数据类型)。
原始数据类型包括:undefined,null,数字,字符串,布尔类型,Symbol(ES6 新加入的类型)
复杂数据类型包括:对象
其中对象包括:内部对象(Array、String等),宿主对象(window)和自定义对象。
Object.prototype.toString.call()
Array.isArray()
相同点:
1.这两个数据类型都只有一个值
2.都没有方法
3.都表示"无"
4.转化为布尔值时,都是false
不同点:
对象转成原始数据类型时,先调用对象的 valueOf
方法,如果返回结果不是原始数据类型的值,再调用 toString
方法。
原始类型转原始类型时,直接调用对应的构造函数进行转换。
包括下面这六种:
0, -0, undefined, null, 空字符串, NaN
Symbol 是 ES6 新加的一个原始类型,它的每个值都是唯一的,即使是用两个完全一样的变量构建出来的Symbol也不相等。
原始类型传入 Symbol 方法时,会进行转换成 字符串
再转成 Symbol 类型值;
如果是对象的话,会先调用对象的 toString
方法再转成 Symbol 类型的值。
Symbol最大的用处是用来消除 "魔术字符串" 的。
split()
Array.from()
Array.prototype.slice.call()
Array.from()
Array.from()的详解:
Set类型的转换
let s = new Set(['foo', window])
Array.from(s);
// ["foo", window]
Map类型的转换
let m = new Map([[1, 2], [2, 4], [4, 8]])
Array.from(m)
// [[1, 2], [2, 4], [4, 8]]
类数组的值
function f() {
console.log(arguments)
return Array.from(arguments)
}
f(1, 2, 3)
Array.from() 的第二个参数 mapFn 也很有用处,可以对传入的类数组值进行定定制化修改
Array.from([1, 2, 3], x => x + x);
// [2, 4, 6]
Array.from({length: 5}, (v, i) => i)
// [0, 1, 2, 3, 4, 5]
setTimeout(function(){
//利用iframe的onload事件刷新页面
document.title = 'test';
var iframe = document.createElement('iframe');
iframe.style.visibility = 'hidden';
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.onload = function () {
setTimeout(function () {
document.body.removeChild(iframe);
}, 0);
};
document.body.appendChild(iframe);
},0);
大家对于 watch 应该不陌生,项目中都用过下面这种写法:
watch: {
someProp() {
// do something
}
}
// 或者
watch: {
someProp: {
deep: true,
handler() {
// do something
}
}
}
上面的写法告诉vue, 我需要监听 someProp 属性的变化,于是vue在内部就会为我们创建一个watcher对象。
然而在vue中,watcher的功能并没有这么单一,先上段代码:
<template>
<div>
<p>a: {{ a }}</p>
<p>b: {{ b }}</p>
<button @click="increment">+</button>
</div>
</template>
<script>
export default {
data () {
return {
a: 1
}
},
computed: {
b () {
return this.a * 2
}
},
watch: {
a () {
console.log('a is changed')
}
},
methods: {
increment () {
this.a += 1
}
},
created () {
console.log(this._watchers)
}
}
</script>
上面代码非常简单,我们现在主要关注 created
钩子中打印的 this._watchers
, 如下:
分别展开三个 watcher
,观察每一个 expression
,从上到下分别为:
- b() {↵ return this.a * 2;↵ }
- "a"
- function () {↵ vm._update(vm._render(), hydrating);↵ }
上面三个 watcher
代表了三种不同功能的 watcher, 我们将其按功能分为三类:
- 在 `watch` 中定义的,用于监听属性变化的 watcher (第二个)
- 用于 `computed` 属性的 watcher (第一个)
- 用于页面更新的 watcher (第三个)
normal-watcher
我们在 watch
中定义的 ,都属于这种类型,即只要监听的属性变化了,都会触发定义好的回调函数
computed-watcher
每一个 computed
属性,最后都会生成一个对应的 watcher
对象,但是这类 watcher 有个特点,就拿上面的 b
举例:
属性 b
依赖 a
, 当 a
改变的时候,b
并不会立即重新计算,只有之后其他地方需要读取 b
的时候,它才会真正计算,即具备 lazy(懒计算)特性
render-watcher
每一个组件都会有一个 render-watcher
, function () {↵ vm._update(vm._render(), hydrating);↵ }
,当 data/computed
中的属性改变的时候,会调用该 render-watcher
来更新组件的视图。
除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:
computed-watcher -> normal-watcher -> render-watcher
这样安排是有原因的,这样就尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-watcher 前面,就会导致页面更新的时候 computed 值为旧数据。
vetur
插件Ctrl + P
然后输入 ext install vetur
然后回车点安装即可
这时可以打开一个vue文件试试,注意下右下角状态栏是否正确识别为 vue 类型:
如果被识别为 text 或 html ,则记得要点击切换下。
vetur
插件安装完 vetur 后还需要加上这样一段配置下:
"emmet.syntaxProfiles": {
"vue-html": "html",
"vue": "html"
}
eslint
插件Ctrl + P
然后输入 ext install eslint
然后回车点安装即可。
ESLint 不是安装后就可以用的,还需要一些环境和配置:
首先,需要全局的 ESLint , 如果没有安装可以使用 npm install -g eslint
来安装。
其次,vue文件是类 HTML 的文件,为了支持对 vue 文件的 ESLint ,需要 eslint-plugin-html
这个插件。可以使用 npm install -g eslint-plugin-html
来安装
接着,安装了 HTML 插件后,还需要在 vscode 中配置下 ESLint:
"eslint.validate": [
"javascript",
"javascriptreact",
"html",
"vue"
],
"eslint.options": {
"plugins": ["html"]
},
最后,别忘了在项目根目录下创建 .eslintrc.json
, 如果还没创建,还可以使用下面快捷命令来创建:
这样一来 vue 中写的 js 代码也能正确地被 lint 了。
vue的生命周期总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。
创建前/后:在beforeCreate阶段,vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,$el还没有。
载入前/后:在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
更新前/后:当data变化时,会触发beforeUpdate和updated方法。
销毁前/后:在执行destoryed方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。
实例代码查看代码源:
vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
第一步:通过Observer提供的接口,对需要observe的数据对象进行递归遍历,给对象的每个属性、子属性对象的属性,都加上setter和getter(都绑定了一个专用的 `Dep`对象,这里的状态对象主要指组件当中的`data`属性)。
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
第二步:compile解析模版指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1.在自身实例化时往属性订阅器(dep)里面添加自己
2.自身必须有一个update()方法
3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模版指令,最终利用Watcher搭起Observer和Compiler之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果。
对应下图:
props
的方式向子组件传递(父子组件)vuex
进行状态管理(父子组件和非父子组件)Vue Event Bus
($emit),使用Vue的实例,实现事件的监听和发布,实现组件之间数据的传递。inheritAttrs
+ $attrs
+ $listeners
附上原文链接Vue2.4版本中新添加的$attrs以及$listeners属性使用 和 Vue.js最佳实践(五招让你成为Vue.js大师)
提示:参考ElementUI组件的实现
提示:this.$set
例如今日头条的tab栏(推荐、视频、社会、音乐、本地...),切换每一个tab,并且滚动到一定高度,返回上一个tab栏,如何保证滚动的高度?
提示: keep-alive、template
如何搭建一个项目或企业的组件库?
不可靠的检测数组方式:
var list = [1, 2, 3]
typeof list // 'object'
Array继承于Object, 所以 typeof 会直接返回 object, 所以不可以用 typeof 来检测。
var list = [1, 2, 3]
list instanceof Array // true
instanceof 表面上看确实是返回了true,但其实并不可靠。原因是Array实质是一个引用,用instanceof方法(包括下面的constructor)都是利用引用地址进行比较的方法来确定的,但是在iframe嵌套的情况下,每一个Array的引用地址都是不同的,比较起来结果也是不确定的,所以这种方法有其局限性。
var list = [1, 2, 3]
list.constructor === Array // true
原因同上。
可靠的检测数组方式:
var list = [1, 2, 3]
Object.prototype.toString.call(list) // [object Array]
var list = [1, 2, 3]
Array.isArray(list) // true
会改变自身的方法:
Array.prototype.copyWithin()
:在数组内部,将一段元素序列拷贝到另外一段元素序列上,覆盖原有的值。
Array.prototype.fill()
:将数组中指定区间的所有元素的值,都替换成某个固定的值。
Array.prototype.pop()
:删除数组最后一个元素,并返回这个元素。
Array.prototype.push()
:在数组的尾部增加一个或多个元素,并返回数组的新长度。
Array.prototype.reverse()
:颠倒数组中元素的排列顺序,即原先的第一个变为最后一个,最后一个变为第一个。
Array.prototype.shift()
:删除数组的第一个元素,返回这个元素。
Array.prototype.sort()
:对数组元素进行排序,并返回当前数组。
Array.prototype.splice()
:在任意的位置给数组添加或删除任意个元素。
Array.prototype.unshift()
:在数组的开头增加一个或多个元素,并返回数组的新长度。
不会改变自身的方法:
Array.prototype.concat()
:返回一个由当前数组和其他若干个数组或若干个非数组值组合而成的新数组。
Array.prototype.includes()
:判断当前数组是否包含某指定的值,如果是返回true,否则返回false。
Array.prototype.join()
:连接所有数组元素组成一个字符串。
Array.prototype.slice()
:抽取当前数组中的一段元素组合成一个新的数组。
Array.prototype.toSource()
:返回一个表示当前数组字面了的字符串。
Array.prototype.toString()
:返回一个由所有数组元素组合而成的字符串。
Array.prototype.toLocaleString()
:返回一个由数组元素组合而成的本地化后的字符串。
Array.prototype.indexOf()
:返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。
Array.prototype.lastIndexOf()
:返回数组中最后一个(从右边第一个)与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。
遍历方法:
Array.prototype.forEach()
:为数组中的每个元素执行一次回调函数。
Array.prototype.entries()
:返回一个对象迭代器对象,该迭代器会包含所有数组元素的键值对。
Array.prototype.every()
:如果数组中的每个元素都满足测试函数,则返回true,否则返回 false。
Array.prototype.some()
:如果数组中至少有一个元素满足测试函数,则返回true,否则返回false。
Array.prototype.filter()
:将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回。
Array.prototype.find()
:找到第一个满足测试函数的元素并返回那个元素的值。如果找不到,则返回undefined。
Array.prototype.findIndex()
:找到第一个满足测试函数的元素并返回那个元素的索引,如果找不到,则返回 -1。
Array.prototype.keys()
:返回数组迭代器对象,该迭代器会包含所有数组元素的健。
Array.prototype.map()
:返回一个由回调函数的返回值组成的新数组。
Array.prototype.reduce()
:从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值。
Array.prototype.reduceRight()
:从右到左为每个元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值。
Array.prototype.values()
:返回数组迭代器,该迭代器会包含所有数组元素的值。
Array.prototype[@@iterator]()
: 和上面的 values() 方法是同一个函数。
Array.prototype.slice.call()
Array.from()
增加了扩展运算符(spread) ...
增加了两个方法,Array.from() 和 Array.of() 方法
增加了一些实例方法,如 copyWithin()、fill()、entries()、keys()、values()、includes() 等。
function unique(arr) {
var tmp = {}, res = [];
arr.forEach(function(item) {
if (!tmp[item]) {
res.push(item);
tmp[item] = true;
}
})
return res;
}
var list = [0, 0, 1, 2, 3, 6, 6]
console.log(unique(list)) // [0, 1, 2, 3, 6]
var list = [0, 0, 1, 2, 3, 6, 6]
console.log([...new Set(list)]) // [0, 1, 2, 3, 6]
Array.prototype 是一个数组,不过 length 为 0
var list = [1, [2, [3]], 4, [5]]
console.log(list.toString()) // 1, 2, 3, 4, 5
原理:toString 方法返回一个字符串,该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。
var list = [1, [2, [3]], 4, [5]]
console.log(list.join()) // 1, 2, 3, 4, 5
原理:join 方法会让所有的数组元素转换成字符串,再用一个分隔符将这些字符串连接起来。如果元素是undefined或null,则会转化成空字符串。
var list = [1, [2, [3]], 4, [5]]
JSON.parse(`[${arr}]`) // [1, 2, 3, 4, 5]
PS: 如果觉得上面输出的不是一个数组,可以稍微加工一下。
var list = [1, [2, [3]], 4, [5]]
JSON.parse(`[${list.toString()}]`)
JSON.parse(`[${list.join()}]`)
JSON.parse(`[${arr}]`)
var arr1 = [1, 2, 3]
var arr2 = arr1.concat();
var arr1 = [1, 2, 3]
var arr2 = arr1.join();
原理:数组本质上也是Object,直接赋值的话,只是将引用赋值给另一个变量,最终会导致被复制的变量也会随着原来的数组变化而变化。
语法:sort方法接收一个 "比较参数" 作为参数。
如果调用该参数时没有使用参数, 将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),再以便进行比较。
如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这个两个值得相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:
若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
若 a 等于 b, 则返回 0。
若 a 大于 b, 则返回一个大于 0 的值。
实现一个冒泡算法
循环遍历一遍:
var list = [1, 100, 23, 65, 43, 2, 9]
var max = list[0]
for(var i = 1; i < list.length; i++) {
max = Math.max(max, list[i])
}
console.log(max) // 100
var list = [1, 100, 23, 65, 43, 2, 9]
function Max(prev, next) {
return Math.max(prev, next)
}
console.log(list.reduce(Max)) // 100
var list = [1, 100, 23, 65, 43, 2, 9]
Math.max.apply(null, list) // 100
var list = [1, 100, 23, 65, 43, 2, 9]
Math.max(...list) // 100
var list = [1, 100, 23, 65, 43, 2, 9]
list.sort((a, b) => {return a - b})
list[list.length - 1] // 100
eval 便是一种:
var list = [1, 100, 23, 65, 43, 2, 9]
var max = eval("Math.max(" + list+ ")")
console.log(max) // 100
解析:
因为 发生了隐式类型转换
例如:
var list = [1, 100, 23, 65, 43, 2, 9]
console.log(list+'') // 1,100,23,65,43,2,9
其实
var max = eval("Math.max("+ list+")")
就相当于
var max = eval("Math.max(1,100,23,65,43,2,9)")
这里切图推荐一个插件:Cutterman,更多切图工具介绍请移步:扶朕起来,朕还能切
Cutterman致力于改善设计师的工作效率,为设计师提供优秀、高效、实用的技术解决方案, 解放双手。让创意不再有界限, 让设计更专注!
Cutterman能够让你只需要点击一个按钮,就自动输出你需要的各种各样的图片,快到没有朋友!
一款全中文免费的自动标注的神器!彻底解放设计师的双手,上传文件就能蹭蹭蹭的自动标注!
现在,这款叫蓝湖的设计师标注神器,最新版开始支持“自动标注”的功能
只需下载“蓝湖”App,即可实现:从Sketch一键导出设计图→自动生成标注→自动共享给团队→团队相关成员自动收到提醒等一系列自动化功能。
蓝湖是一款产品设计师的协作平台,帮助设计师更快地完成工作。蓝湖通过帮助设计师更好地向团队展示设计图,描述页面之间的跳转关系。蓝湖还支持从Sketch一键分享、在线协作…
“自动标注”功能可以完整而清晰地将Sketch设计图中每个元素的尺寸、位置、颜色、间距、字号 等样式信息自动同步到蓝湖,团队内的工程师等同事可以随时查看。
如果设计图出现改动和更新,蓝湖也能自动添加新版本。
如今设计师的工具那么多,这一款工具的优势在哪里呢
1.所有功能完全免费,没有任何项目或团队成员数量限制。
2.中文的!中文的!中文的!
3.无与伦比的快!在国内的服务器+蓝湖工程师呕心沥血优化的算法,使蓝湖的“自动标注”的速度嗖嗖的!
4.蓝湖还整合了设计图流程的展示,设计图历史版本管理,多种情况和状态的设计图管理等功能。
5.设计师不但可以为每张设计图添加备注文档,其他团队成员还可以针对设计图发表评论,方便团队在线高效沟通。(内心竟有点小小的惶恐…)
6.在蓝湖上,还可以基于设计图快速制作一个高保真的交互原型,让工程师不用再跑来问你“这个按钮跳到哪啊”,该原型还可以在蓝湖手机端App和微信上进行操作和预览。
蓝湖主体功能是Web端网页平台,不需要下载,直接注册就可以免费使用。
随着sketch的普及(sketch是啥,能吃吗?自行谷歌、必应),国内外很多项目团队都陆续用起了sketch+zeplin的开发模式。不过话说回来,sketch真的有那么好用吗?很多小伙伴们表示用ps好几年了,要我重新学一个软件,臣妾做不到啊!~
其实刚进公司的时候也是这种心情的,没用过mac更没用过sketch,完全的小白用户,当时内心几乎是奔溃的。但是自从接触sketch后,真的是爱不释手,都不想用回ps了。
在使用 Zeplin 之前,最早是使用马克曼(手动标注,这里不做推荐)进行标注的,也就是直接在输出效果图上量尺寸;使用 Sketch 插件 Measure 之后,可以在画板中生成尺寸标注信息,导出标注图提供给开发同学使用。无论是马克曼还是 Measure,最后的交付物是一致的,马克曼和接下来要介绍的Measure这种原始的标准就是已经破坏了原本的视觉效果图,标注的信息一定会对原设计稿形成遮挡,因此一般效果图和标注图要分开给,开发也经常需要在两个图之间切换,图片管理不太方便。
zeplin 主要就是为了解决上述问题的,使用它之后,可以在 Sketch 中一键导入 Artboard,在设计师做好图层管理(命名、分组)的前提下,它可以自动生成标注信息(并且可以标注为 pt 或 dp),允许添加注释形成类 prd 文档,并且自动提取 Style Guide,同时还允许添加项目组成员,提供给团队组查看项目。
介绍之后回答两个基本问题
答:不好意思,目前没有!设计师为了提升工作效率,就算吃土一两个月也要买台mac。不过windows用户除了装mac虚拟机外,现在ps也支持zeplin插件了,只要安装个插件,没有mac也照样可以任性的告别切图和标注。
答:真够意思,这个必须有!不久前只有mac版,不过zeplin团队怎么会放弃windows那么大块的市场呢。真是喜大普奔,现在zeplin也支持windows了,从此开发哥哥再也不会抱怨网页端的zeplin打开速度超级慢了。
好了,废话不多说,直接进正题。
1、sketch支持多画板,便于同时预览,占用内存较ps小很多;
2、sketch支持导出flinto,便于制作交互动效原型;
3、zeplin解放设计师的双手,从此告别切图和标注;
4、zeplin降低工程师的沟通成本,提高设计还原度。
更多细节已经安装方法导出技巧请移步:APP标注无烦恼!告别切图标注-Sketch/PS+Zeplin
这里这介绍工具,由于篇幅有限,并不详细教你怎么用,工具多用用就会了,熟能生巧。
更多关于Zeplin的体验和细节请移步:Zeplin 的使用体验如何?
Sketch Measure是一款可用于标注和设计规范的工具,支持Sketch 3.5版以上。Measure帮你解放你的双手…
1.创建叠加
2.度量尺寸
3.度量边距
4.获取属性
5.添加注释
关于Sketch Measure的使用教程,这里也不多细说,这里抛砖引玉的介绍一下,想要了解和使用请移步:Sketch Measure切图标注插件使用教程
下面谈一谈Zeplin和Sketch Measure的区别,纯属引用,表示没用过Sketch Measure:
①Zeplin注册免费,只能保留一个Active项目,“STARTER”17刀/月,3个Active项目,“GROWING BUSINESS”26刀/月,12个Active项目。“ORGANIZATION”每个用户6.75刀/月。
②支持MAC的Sketch和PS,以及PC的PS。(最大的优点)
③数据必须上传到网络上,可以用客户端查看也可以网页查看,必须是注册用户。(很麻烦,有些公司不允许上传就没办法了)
④自动生成styleguide。(非常棒)
⑤切图需要查看相应页面时,从切图栏下载。(我用的并不多,也可能有其他方式)
①完全免费。
②只支持MAC Sketch,但查看不受限制。
③数据保存在本地(html文件),方便打包后发邮件,缺点是每次更新都要再发一遍,管理麻烦。
④没有Zeplin智能,没有自动styleguide,但是有类似AssistorPS一样的手动标注。
⑤有“颜色命名”但比styleguide差很多,希望以后能更新类似功能。自动打包输出切图,支持iOS和Android的命名方式。
这是一款在线的智能标注工具,属于国产软件,可以及时动态查看设计稿的相关标注信息,是最近新出的软件。
优点:
缺点:
标你妹适合小型的个人的一些项目,对于新手来说,学习成本基本为0,非常方便,web端没有平台限制;
蓝湖国内的良心之作,速度很快,适合个人和企业协同合作开发;
Zeplin适合小型的团队,还带有一部分协作办公的功能(留言和更新状况),要求前端也能适应这种新的方式;
Sketch Measure更传统一些,本地文档、打包切图等等,更适合有自己办公流程的大公司,仅仅支持Mac。
iDoc国内的良心之作,值得一试;
class Widget {
// 共有方法
foo(baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf= baz
}
}
上面代码中, _bar
方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。
利用 Symbol
值 的唯一性,将私有方法的名字命名为一个 Symbol
值。
class Fruit {
constructor() {
const number = Symbol('number');
class F {
constructor() {
this[number] = 1;
}
getNumber() {
return this[number];
}
setNumber(num) {
this[number] = num;
}
}
return new F()
}
}
const apple = new Fruit()
apple.getNumber() // 1
apple.setNumber(5)
apple.getNumber(5) // 5
apple[number] // Uncaught ReferenceError: number is not defined
与私有方法一样,ES6不支持私有属性。目前,有一个提案,为 class
加了私有属性。方法是在属性名之前,使用 #
表示。
class Point {
#x;
constructor(x = 0) {
#x = +x; // 写成 this.#x 亦可
}
get x() {
return #x;
}
set x(value) {
#x = +value;
}
}
上面代码中,#x
就表示私有属性 x
,在 Point
类之外是读取不到这个属性的。还可以看到,私有属性与实例的属性是可以同名的(比如,#x
与 get x()
)
Vue 和 React 的区别?
[https://www.zhihu.com/question/31585377](https://www.zhihu.com/question/31585377)
如果你的组件有state或者使用了生命周期函数,那么请使用 Class Component。否则,使用 Functional Component。
Refs是你访问DOM元素或者组件实例的一个安全门。为了使用它们,你可以在组件上加上一个 ref 属性,ref的值是一个回调函数,这个回调函数接受底层的DOM元素或者被挂载的组件实例作为它的第一个参数。
class CustomForm extends Component {
handleSubmit = () => {
console.log("Input Value: ", this.input.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type='text' ref={input => this.input = input} />
<button type='submit'>Submit</button>
</form>
)
}
}
从上述所知,我们的输入框有一个ref属性,它的值是一个函数。这个函数接受这个input对应的真实DOM元素,我们绑定到this后得到该实例以在handleSubmit这个方法里访问它。
人们常常会误解,为了使用refs必须使用 Class Component, 单实际refs还可以通过闭包在 Functional Component 中使用。
function CustomForm({handleSubmit}) {
let inputElement
return (
<form onSubmit={() => handleSubmit(inputElement.value)}>
<input type='text' ref={input => inputElement = input} />
<button type='submit'>Submit</button>
</form>
)
}
keys负责帮助React跟踪列表中哪些元素被改变/添加/删除。
Ajax请求应该在 componentDidMount
函数中进行。
本文的代码都在 https://github.com/riskers/docker-demo 可以找到。
对于我们前端来说,这样比较好理解:构建好的镜像只要就是 package.json
,里面只要写好需要的包名和版本号,任何人拿到这份文件都可以获取对应的包,并且能够运行这个程序。但是,我们忘了一个很重要的点,Node 和 npm 也是有版本的啊!举个例子,Node 在 7.x 以上才支持 async
,如果我们的服务器上 Node 的版本是 4.x ,那么部署上去肯定是不行的,然后查个半天 BUG ,才发现是 Node 版本的问题。Docker 就是帮我们解决这种问题的,它能够把 Node 版本也能够像 package.json
那样记录在配置文件中。
判断 docker 是否能正常工作
docker info # 返回所有容器和镜像的数量、基本配置等
运行容器
docker run image-name
比如 docker run ubuntu
,会先检查本地是否存在 ubuntu 镜像,如果本地没有该镜像的话,那么 Docker 就会查看 Docker Hub 中是否有该镜像,找到的话就会下载该镜像并将其保存到本地。
随后,Docker 在文件系统内部用这个镜像创建了一个新容器,该容器拥有自己的网络、IP地址,以及一个用来和宿主机进行通信的桥接网络接口。
使用容器
docker run -t -i ubuntu
会进入容器,然后 exit
离开容器,容器就停止运行了。
-i: Keep STDIN open even if not attached
-t: Allocate a pseudo-tty
但容器还是存在的,docker ps -a
查看所有容器(正在运行的、已经停止的)
docker ps
只会列出正在运行的容器。
容器命名
Docker 会为每一个容器自动生成一个随机 id,我们也可以自己为容器指定名称。
docker run --name your_container_name -i -t ubuntu
容器名称不允许同名,可以使用 docker rm
删除同名容器。
重新启动已经停止的容器
docker start your_container_name / your_container_id
docker restart your_container_name / your_container_id
获取信息
docker inspect your_container_name / your_container_id
附着到容器(重新启动并运行一个交互式会话shell)
docker attach your_container_name
守护容器
docker run -d your_container_name #创建守护容器
docker top your_container_name #查看容器内进程
docker exec your_container_name touch a.txt #在容器内部运行进程
docker stop your_container_name #停止容器
删除容器
docker rm your_container_name/id
列出镜像
docker images
拉取镜像
docker pull
docker store 上每个镜像都有很多个标签
docker pull ubuntu:12.04
表示拉取 12.04 这个标签
构建镜像(Dockerfile + docker build)
一般来说,我们不是真正地创建新镜像,而是基于一个已有的基础镜像,构建新镜像而已。
FROM ...
RUN ...
# 指定容器内的程序将会使用容器的指定端口
# 配合 docker run -p
EXPOSE ...
docker history images [name]
从新镜像启动容器
docker run -d -p 4000:80 --name [name]
可以在 Dokcer 宿主机上指定一个具体的端口映射到容器的80端口上
推送镜像
docker push [user_name]/[image_name]
删除镜像
docker rmi [user_name]/[image_name]
Function.prototype.apply()
可以让提供的 this
与数组参数来调用函数
var numbers = [1,2,3,4]
Math.max.apply(null, numbers) // 4
Math.min.apply(null, numbers) // 1
var numbers = [1,2,3,4]
Math.max(...numbers) // 4
Math.min(...numbers) // 1
在 ES6 中新增了两种数据结构,它们分别是 Set 和 Map,我们可以分别将它们和 Array、Object 进行对比
Set 的字面意思就是集合,与 Array 类似,都是一些成员的聚集
根据中学数学我们知道,集合中的所有元素都是唯一的,所以 Set 和 Array 之间最大的区别是:Set中所有的成员都是唯一的
const s1 = new Set()
s1.add(1)
s1.add(2)
s1.add(1)
// s1 的值为 {1, 2}
Set 函数可以接受一个可遍历的数据结构作为参数来初始化
const s2 = new Set([1,2,1,3,4])
// s2 的值为 {1, 2, 3, 4}
可遍历的数据结构有哪些呢?数组、类数组、含有 iterable 接口 的其他数据结构都是可遍历的
size
s2.size // 4
add(value)
向 Set 中添加一个值,返回 Set 结构本身
s2.add(5).add(6).add(7)
delete(value)
删除一个值,返回布尔值表示是否成功删除
s2.delete(7)
has(value)
Set 中是否包含某个值,返回布尔值
s2.has(1)
clear()
清除所有成员,无返回值
s2.clear()
可以通过forEach进行遍历
s1.forEach((value, key) => console.log(value, key))
由于 Set 没有键名,只有键值,所以 key 和 value 是同一个值
Object 是键值对的集合,而其中的键只能使用字符串,而 Map 与 Object 类似,也是键值对的集合,但是 Map 的键可以是何种类型的值
const m1 = new Map()
const k = {key: 'value'}
m1.set(k, 'mapValue')
m1.get(k)
Map 可以接受一个数组作为参数,这个数组的成员是一个个表示键值对的数组
const m2 = new Map([
['name', 'xiaoming'],
['age', 18]
])
// {'name'=>'xiaoming', 'age'=>18}
size
m2.size // 2
set(key, value)
设置一个键名为 key 键值为 value 的成员,并返回整个 Map 结构,如果已经存在该键名,则会更新键值
m2.set('sex', 'boy')
get(key)
读取这个 key 对应的值,如果没有则返回 undefined
m2.get('sex')
has(key) / delete(key) / clear()
使用方法可参照上文 Set 对应的方法使用方式
keys() / values() / entries()
这三个遍历器生成函数分别返回 键名的遍历器、键值的遍历器、所有成员的遍历器
for (let key of m2.keys()) {console.log(key)}
for (let value of m2. values()) {console.log(value)}
for (let item of m2. entries()) {console.log(item[0], item[1])}
// 这里的 item 是一个数组,其中第一个值为键名,第二个值为键值
forEach()
m2.forEach((value, key) => console.log(value, key))
最简单的方法是,给你将要加载的图片在样式里写好宽高,这样在加载完样式时就已经直到资源图片的占位。
然而到了移动端,需要适配不同的屏幕,图片就不能写死宽高了,我们希望它撑满宽度,高度自适应。可以使用 padding-bottom
或 padding-top
设置百分比来 hack。
@mixin aspect-ratio($width, $height) {
position: relative;
padding-bottom: ($height / $width) * 100%;
img,
video,
iframe,
object,
embed {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.ratio-sixteen-nine {
@include aspect-ratio(1600, 900);
}
<figure class="ratio-sixteen-nine">
<img src="waterfall.jpg" alt="Waterfall in Iceland">
</figure>
tips: 当你设置 padding 或者 margin 垂直方向为百分比时,它们的参照物是包含块的宽度,不是高度!不是高度!不是高度!
还有些时候,我们并不知道将要加载的内容是什么,无法提前获取资源的信息。这种情况也能稍微补救一下,比如说给资源设置一个 min-height 避免大幅度跳动、或者加上一个 transition ,加载前宽度设为 0, 加载完成之后,高度自适应,让用户知道接下来目光要转向何处。
还有一种是修改文字粗细时,容易导致宽度增大,引起的文本跳动。
解决方法可以是使用伪元素来撑开宽度,然后将其隐藏。
.bar:hover {
font-weight: bold;
}
.bar:after {
display: block;
content: attr(data-title);
font-weight: bold;
height: 1px;
color: white;
color: transparent;
overflow: hidden;
visibility: hidden;
}
1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this 。
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
关键
text-align: justify
after、before伪元素
<!DOCTYPE html>
<html>
<head>
<title>demo</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.row>div:first-of-type {
/* width: 500px; */
height: 50px;
line-height: 50px;
font-size: 12px;
border: 1px solid red;
text-align: justify;
}
.row>div:first-of-type:after {
content: " ";
display: inline-block;
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-xs-4 col-sm-4 col-lg-4">姓名</div>
<div class="col-md-8 col-xs-8 col-sm-8 col-lg-8"></div>
</div>
<div class="row">
<div class="col-md-4 col-xs-4 col-sm-4 col-lg-4">手机号码</div>
<div class="col-md-8 col-xs-8 col-sm-8 col-lg-8"></div>
</div>
<div class="row">
<div class="col-md-4 col-xs-4 col-sm-4 col-lg-4">验证码</div>
<div class="col-md-8 col-xs-8 col-sm-8 col-lg-8"></div>
</div>
<div class="row">
<div class="col-md-4 col-xs-4 col-sm-4 col-lg-4">密码</div>
<div class="col-md-8 col-xs-8 col-sm-8 col-lg-8"></div>
</div>
</div>
</body>
</html>
最简单的清空和截短数组的方法就是改变 length
属性:
const arr = [11, 22, 33, 44, 55, 66]
// 截取
arr.length = 3
console.log(arr) // => [11, 22, 33]
// 清空
arr.length = 0
console.log(arr) // => []
console.log(arr[2]) // => undefined
以前,当我们希望向一个函数传递多个参数时,可能会采用配置对象
的模式:
doSomething({ foo: 'hello', bar: 'Hey!', baz: 42 })
function doSomething(config) {
const foo = config.foo !== undefined ? config.foo : 'Hi'
const bar = config.bar !== undefined ? config.bar : 'Yo!'
const baz = config.baz !== undefined ? config.baz : 13
}
这是一个古老但是有效的模式,有了ES2015
的对象结构,你可以这样使用:
function doSomething({ foo = 'Hi', bar = 'Yo!', baz = 13 }) {}
如果你需要这个配置对象
参数变成可选的,也很简单:
function doSomething({ foo = 'Hi', bar = 'Yo!', baz = 13 } = {}) {}
使用对象解构将数组项赋值给变量:
const csvFileLine = '1997,John Doe,US,[email protected],New York'
const { 2: country, 4: state } = csvFileLine.split(',')
注:本例中,
2
为split
之后的数组下标,country
为指定的变量,值为US
switch
语句中使用范围这是一个在switch
语句中使用范围的例子:
function getWaterState(tempInCelsius) {
let state
switch (true) {
case tempInCelsius <= 0:
state = 'Solid'
break
case tempInCelsius > 0 && tempInCelsius < 100:
state = 'Liquid'
break
default:
state = 'Gas'
}
return state
}
await
多个async
函数await
多个 async
函数并等待他们执行完成,我们可以使用 Promise.all
:
await Promise.all([anAsyncCall(), thisIsAlsoAsync(), oneMore()])
你可以创建一个 100% 的纯对象,这个对象不会继承 Object
的任何属性和方法(比如 constructor
, toString()
等):
const pureObject = Object.create(null)
console.log(pureObject) //=> {}
console.log(pureObject.constructor) //=> undefined
console.log(pureObject.toString) //=> undefined
console.log(pureObject.hasOwnProperty) //=> undefined
JSON
代码JSON.stringify
不仅可以字符串
化对象,它也可以格式化你的JSON
输出:
const obj = {
foo: { bar: [11, 22, 33, 44], baz: { bing: true, boom: 'Hello' } }
}
// 第三个参数为格式化需要的空格数目
JSON.stringify(obj, null, 4)
// =>"{
// => "foo": {
// => "bar": [
// => 11,
// => 22,
// => 33,
// => 44
// => ],
// => "baz": {
// => "bing": true,
// => "boom": "Hello"
// => }
// => }
// =>}"
使用ES2015
和扩展运算符,你可以轻松移除数组中的重复项:
const removeDuplicateItems = arr => [...new Set(arr)]
removeDuplicateItems([42, 'foo', 42, 'foo', true, true])
//=> [42, "foo", true]
注:只适用于数组内容为
基本数据类型
使用扩展运算符可以快速扁平化数组:
const arr = [11, [22, 33], [44, 55], 66]
const flatArr = [].concat(...arr) //=> [11, 22, 33, 44, 55, 66]
不幸的是,上面的技巧只能适用二维数组
,但是使用递归,我们可以扁平化任意维度数组:
function flattenArray(arr) {
const flattened = [].concat(...arr)
return flattened.some(item => Array.isArray(item))
? flattenArray(flattened)
: flattened
}
const arr = [11, [22, 33], [44, [55, 66, [77, [88]], 99]]]
const flatArr = flattenArray(arr)
//=> [11, 22, 33, 44, 55, 66, 77, 88, 99]
constructor -> static getDerivedStateFromProps -> render -> (更新DOM和Refs) -> componentDidMount
static getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> (更新DOM和Refs) -> componentDidUpdate
componentWillUnmount
componentDidCatch
var judgeObj = function(obj) {
for(var item in obj) {
return false;
}
return true;
}
2.通过JSON自带的.stringify方法来判断。
var judgeObj = function(obj) {
if(JSON.stringify(obj) === '{}') return true;
return false;
}
var judgeObj = function(obj) {
if(Object.keys(obj).length === 0) return true;
return false;
}
语法:JSON.stringify(value[,replacer[, space]])
传入一个JSON格式的JS对象或数组:
// 对象
JSON.stringify({"name": "Good Man", "age": 18})
// '{"name": "Good Man", "age": 18}'
// 数组
JSON.stringify([1, 2, 3, "4"])
// '[1, 2, 3, "4"]'
var friend = {
"firstName": "Good",
"lastName": "Man",
"phone": "1234567",
"age": 18
}
var friendAfter = JSON.stringify(friend, function (key, value) {
if (key === 'phone') {
return "(000)" + value;
} else if (typeof value === 'number') {
return 10 + value;
} else {
return value; // 如果你把这个else分句(即是缺少最后的return value),那么结果会是 undefined
}
})
console.log(friendAfter);
// {"firstName": "Good", "lastName": "Man", "phone": "(000)1234567", "age": 28}
如果上面的传入不是键值对的对象形式,而是数组形式的,比如上面传入的是
var friend = ["Jack", "Rose"]
, 如果是数组形式,那么 key 是索引值,而 value 是这个数组项,记得要在函数内部返回value,不然会出错。
var friend = {
"firstName": "Good",
"lastName": "Man",
"address": "1234567",
"age": 18,
"phone": {"home": "1234567", "work": "7654321"}
}
var friendAfter = JSON.stringify(friend, ["firstName", "address", "phone"])
console.log(friendAfter);
// {"firstName": "Good", "address": "1234567","phone": {}}
// 由于 phone 对象的属性不在数组中,所以没有被序列化出来
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.