白霸天的博客


  • 首页

  • 归档

  • 标签
白霸天的博客

你需要知道的单页面路由实现原理

发表于 2018-04-28 |

前言

最近开发的埋点项目,需要记录用户行为轨迹即用户页面访问顺序。需要在页面跳转的时候,记录用户访问的信息(比如 url ,请求头部等),非单页面应用可以给 window 对象加上一个 beforeunload 事件,在页面离开时触发采集开关,但是现在很多业务是单页面应用,用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。所以单页面应用的路由插件一定运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

pushState 和 replaceState 了解一下

history 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录

history 对象的详细信息已经有很多很好很详细的介绍文献,这里不再做总结,我们引用阮老师的教程介绍,history对象 – JavaScript 标准参考教程(alpha)

history.pushState

history.pushState方法接受三个参数,依次为:

state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

    var stateObj = { foo: 'bar' };
    history.pushState(stateObj, 'page 2', '2.html');

添加上面这个新记录后,浏览器地址栏立刻显示 example.com/2.html,但并不会跳转到 2.html,甚至也不会检查2.html 是否存在,它只是成为浏览历史中的最新记录。这时,你在地址栏输入一个新的地址(比如访问 google.com ),然后点击了倒退按钮,页面的 URL 将显示 2.html;你再点击一次倒退按钮,URL 将显示 1.html。

总之,pushState 方法不会触发页面刷新,只是导致 history 对象发生变化,地址栏会有反应。

如果 pushState 的 url参数,设置了一个新的锚点值(即hash),并不会触发 hashchange 事件。如果设置了一个跨域网址,则会报错。

    // 报错
    history.pushState(null, null, 'https://twitter.com/hello');

    上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。

history.replaceState

history.replaceState 方法的参数与 pushState 方法一模一样,区别是它修改浏览历史中当前纪录,假定当前网页是 example.com/example.html。

    history.pushState({page: 1}, 'title 1', '?page=1');
    history.pushState({page: 2}, 'title 2', '?page=2');
    history.replaceState({page: 3}, 'title 3', '?page=3');

    history.back()
    // url显示为http://example.com/example.html?page=1

    history.back()
    // url显示为http://example.com/example.html

    history.go(2)
    // url显示为http://example.com/example.html?page=3

单页面应用用户访问轨迹埋点

开发过单页面应用的同学,一定比较清楚,单页面应用的路由切换是无感知的,不会重新进行 http 请求去获取页面,而是通过改变页面渲染视图来实现。所以他的实现原理一定也是通过原生的 pushState 或则 replaceState 来实现的。所以在页面跳转的时候一定会调用 pushState 或则 replaceState ,要记录用户的跳转信息,我们只要拦截 pushState 和 replaceState,在执行默行为前先执行我们的方法就能够采集到用户的跳转信息了


    // 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

    collect = {}

    collect.onPushStateCallback : function(){}  // 自定义的采集方法

    (function(history){

        var replaceState = history.replaceState;   // 存储原生 replaceState

        history.replaceState = function(state, param) {     // 改写 replaceState
           var url = arguments[2];

           if (typeof collect.onPushStateCallback == "function") {
                 collect.onPushStateCallback({state: state, param: param, url: url});   //自定义的采集行为方法
           }

           return replaceState.apply(history, arguments);    // 调用原生的 replaceState
        };
     })(window.history);


vue-router 的路由实现

既然知道了这个原理,我们来看下 vue-router 的实现,我们打开 vue-router 项目地址,把项目克隆下来,或则直接在 github 上预览,在 Vue 开发的项目里,我们通过 router.push(‘home’) 来实现页面的跳转,所以我们检索下,push 方法的实现

push方法检索

我们检索到了 20 个 js 文件,😂,一般到这个时候,我们会放弃源码阅读,那么我们今天的文章就到这结束,谢谢大家!

开个玩笑,源码阅读不能这么粗糙,我们找到 src 目录,点开 index.js 文件,看到 history对象的定义和 mode 参数有关

 if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }

看到 history 对象的实例与配置的 mode 有关,vue-router 通过3中方式实现了路由切换。与我们今天讲的内容相匹配的是 HTML5History 的实现方案,其他的将不再文章中做扩展,若果你感兴趣想要了解,可以看文章后面的扩展阅读

我们来看 vue-router 中的 HTML5History 源码:

    push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
      const { current: fromRoute } = this
      this.transitionTo(location, route => {
        pushState(cleanPath(this.base + route.fullPath))
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      }, onAbort)
    }

    replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
      const { current: fromRoute } = this
      this.transitionTo(location, route => {
        replaceState(cleanPath(this.base + route.fullPath))
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      }, onAbort)
    }

    // src/util/push-state.js
    export function pushState (url?: string, replace?: boolean) {
      saveScrollPosition()
      // try...catch the pushState call to get around Safari
      // DOM Exception 18 where it limits to 100 pushState calls
      const history = window.history
      try {
        if (replace) {
          history.replaceState({ key: _key }, '', url)
        } else {
          _key = genKey()
          history.pushState({ key: _key }, '', url)
        }
      } catch (e) {
        window.location[replace ? 'replace' : 'assign'](url)
      }
    }

    export function replaceState (url?: string) {
      pushState(url, true)
    }

在使用 Vue 开发的过程中,我们一定用到过 push 和 replace 来改变路由,和视图。

router 实例调用的 push 实际是 history 的方法,通过 mode 来确定匹配 history 的实现方案,从代码中我们看到,push 调用了 src/util/push-state.js 中被改写过的 pushState 的方法,改写过的方法会根据传入的参数 replace?: boolean来进行判断调用 pushState 还是 replaceState ,同时做了错误捕获,如果,history 无刷新修改访问路径失败,则调用 window.location.replace(url) ,有刷新的切换用户访问地址 ,同理 pushState 也是这样。这里的 transitionTo 方法主要的作用是做视图的跟新及路由跳转监测,如果 url 没有变化(访问地址切换失败的情况),在 transitionTo 方法内部还会调用一个 ensureURL 方法,来修改 url。 transitionTo 方法中应用的父方法比较多,这里不做长篇赘述,具体代码分析可以关注后我以后的文章

模拟单页面路由

通过上面的学习,我们知道了,单页面应用路由的实现原理,我们也尝试去实现一个。在做管理系统的时候,我们通常会在页面的左侧放置一个固定的导航 sidebar,页面的右侧放与之匹配的内容 main 。点击导航时,我们只希望内容进行更新,如果刷新了整个页面,到时导航和通用的头部底部也进行重绘重排的话,十分浪费资源,体验也会不好。这个时候,我们就能用到我们今天学习到的内容,通过使用 HTML5 的 pushState 方法和 replaceState 方法来实现,

思路:首先绑定 click 事件。当用户点击一个链接时,通过 preventDefault 函数防止默认的行为(页面跳转),同时读取链接的地址(如果有 jQuery,可以写成$(this).attr(‘href’)),把这个地址通过pushState塞入浏览器历史记录中,再利用 AJAX 技术拉取(如果有 jQuery,可以使用$.get方法)这个地址中真正的内容,同时替换当前网页的内容。

为了处理用户前进、后退,我们监听 popstate 事件。当用户点击前进或后退按钮时,浏览器地址自动被转换成相应的地址,同时popstate事件发生。在事件处理函数中,我们根据当前的地址抓取相应的内容,然后利用 AJAX 拉取这个地址的真正内容,呈现,即可。

最后,整个过程是不会改变页面标题的,可以通过直接对 document.title 赋值来更改页面标题。

扩展

好了,我们今天通过多个方面来讲了 pushState 方法和 replaceState 的应用,你应该对这个两个方法能有一个比较深刻的印象,如果想要了解更多,你可以参考以下链接

history对象 – JavaScript 标准参考教程(alpha)
从vue-router看前端路由的两种实现

白霸天的博客

你需要知道的 javascript 的细节

发表于 2018-04-02 |

现在的前端框架层出不穷,3个月就要重新入门一次前端的现状,让我们来不及学好基础就开始上手框架。常常就因为这样,我们会很快到达基础技术瓶颈,基础是所有技术的核心,在跳槽季重新温故了一遍 javascript 基础,有收获,整理出来分享给大家。

对象

变量可以当对象使用

javascript 中所有的变量都可以当做对象使用,除了undefined 和 null ,我们测试下

1
2
3
4
5
6
7
false.toString() // "false"
[1,2,3].toString() //"1,2,3"
1..toString() //"1"
({a:'33'}).toString() //"[object Object]"
1
2
3
undefined.toString() //Uncaught TypeError
null.toString() //Uncaught TypeError

数值和对象虽然能调用 toString 方法,但是在写法上需要注意下

number 调用时不能直接数值后面直接调用toString 方法,因为 js 会将点运算符解析为数值的小数点

1
2
3
1.toString() //Uncaught SyntaxError
1..toString() //"1"

对象直接调用toString 方法时,需要用小括号包裹起来,不然js 会将对象的花括号识别成块,从而报错

1
2
3
{a:'33'}.toString() // Uncaught SyntaxError
({a:'33'}).toString() // "[object Object]"

对象删除属性

删除对象的属性唯一的方法是使用 delete 操作符,设置元素属性为 undefined 或则 null 并不能真正删除,只是移除了属性和值的关联

1
2
3
4
5
6
7
8
9
10
11
12
13
var test = {
name:'bbt',
age:'18',
love:'dog'
}
test.name = undefined
test.age = null
delete test.love
for (var i in test){
console.log(i+':'+test[i])
}

运行结果

1
2
3
name:undefined
age:null
undefined

只有 love 被正则删除,name 和 age 还是能被遍历到

构造函数

在 javascript 中,通过关键字 new 调用的函数就被认为是构造函数,我们可以通过构造函数创建对象实例

但是在使用过程中你一定发现了,每实例化一个对象,都会在实例对象上创造构造函数的方法和属性。倘若创建的实例比较多,重复创建同一个方法去开辟内存空间就会显得十分浪费,我们可以通过把被经常复用的方法放在原型链上。

原型继承

javascript 和一些我们所了解的面向对象编程的语言不太一样,在 es6 语法以前,我们是通过原型链来实现方法和属性的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Child(){
this.name = 'bbt'
}
Child.prototype = {
title:'baba',
method: function() {}
};
function Grandson(){}
//设置 Grandson 的 prototype 为 Child 的实例
Grandson.prototype = new Child()
//为 Grandson 的原型添加添加属性 age
Grandson.prototype.age = 40
// 修正 Grandson.prototype.constructor 为 Grandson 本身
Grandson.prototype.constructor = Grandson;
var xiaomin = new Grandson()
//原型链如下
xiaomin // Grandson的实例
Grandson.prototype // Child的实例
Grandson.prototype //{title:'baba',...}
Object.prototype
{toString: ... /* etc. */};

对象的属性查找,javascript 会在原型链上向上查找属性,直到查到 原型链顶部,所以,属性在原型链的越上端,查找的时间会越长,查找性能和复用属性方面需要开发者自己衡量下。

获取自身对象属性

hasOwnProperty 方法能够判断一个对象是否包含自定义属性,而不是在原型链上的属性

1
2
3
4
5
6
7
var test = {hello:'123'}
Object.prototype.name = 'bbt'
test.name //'bbt'
test.hasOwnProperty('hello') //true
test.hasOwnProperty('name') //false

for in 循环可以遍历对象原型链上的所有属性,如此我们将 hasOwnProperty结合循环for in 能够获取到对象自定义属性

1
2
3
4
5
6
7
8
9
10
11
12
13
var test = {hello:'222'}
Object.prototype.name = 'bbt'
for(var i in test){
console.log(i) // 输出两个属性,hello ,name
}
for(var i in test){
if(test.hasOwnProperty(i)){
console.log(i)//只输出 hello
}
}

除了上面的方法,getOwnPropertyNames 和 Object.keys 方法,能够返回对象自身的所有属性名,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名。

1
2
3
4
5
var test = {hello:'222'}
Object.prototype.name = 'bbt'
Object.keys(test) //["hello"]
Object.getOwnPropertyNames(test) //["hello"]

那 getOwnPropertyNames 和 Object.keys 的用法有什么区别呢

Object.keys方法只返回可枚举的属性,Object.getOwnPropertyNames 方法还返回不可枚举的属性名。

1
2
3
4
5
var a = ['Hello', 'World'];
Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a) // ["0", "1", "length"] // length 是不可枚举属性

函数

函数声明的变量提升

我们通常会使用函数声明或函数赋值表达式来定义一个函数,函数声明和变量声明一样都存在提升的情况,函数可以在声明前调用,但是不可以在赋值前调用

函数声明

1
2
foo(); // 正常运行,因为foo在代码运行前已经被创建
function foo() {}

函数表达式

1
2
3
foo; // 'undefined'
foo(); // 出错:TypeError
var foo = function() {};

变量提升是在代码解析的时候进行的,foo() 方法调用的时候,已经在解析阶段将 foo 定义过了。赋值语句只在代码运行时才进行,所以在赋值前调用会报错

一种比较少用的函数赋值操作,将命名函数赋值给一个变量,此时的函数名只对函数内部可见

1
2
3
4
5
var test = function foo(){
console.log(foo) //正常输出
}
console.log(foo) //Uncaught ReferenceError

this 的工作原理

在 javascript 中 ,this 是一个比较难理解的点,不同的调用环境会导致 this 的不同指向,但是唯一不变的是 this 总是指向一个对象

简单的说,this 就是属性和方法当前所在的对象(函数执行坐在的作用域),平时使用的 this 的情况可以大致分为5种

调用方式 指向
1. 全局范围调用 指向 window 全局对象
2. 函数调用 指向 window 全局变量
3. 对象的方法调用 指向方法调用的对象
4. 构造函数调用 指向构造函数创建的实例
5. 通过,call ,apply ,bind 显示的指定 this指向 和传参有关

Function.call

语法:function.call(thisArg, arg1, arg2, …),thisArg表示希望函数被调用的作用域,arg1, arg2, …表示希望被传入函数额参数 , 如果参数为空、null和undefined,则默认传入全局对象。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'xiaomin'
var test = {name : 'bbt'}
function hello( _name ){
_name ?console.log(this.name,_name): console.log(this.name)
}
hello() //xiaomin
hello.call(test) //bbt
hello.call(test,'xiaohong') //bbt xiaohong
hello.call() //xiaomin
hello.call(null) //xiaomin
hello.call(undefined) //xiaomin

Function.apply

语法和call 方法类似,不同的是,传入调用函数的参数变成以数组的形式传入,即 func.apply(thisArg, [argsArray])

改造上面的示例就是

1
hello.apply(test,['xiaomin'])

Function.bind

bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

1
2
3
4
5
var d = new Date();
d.getTime()
var print = d.getTime; //赋值后 getTime 已经不指向 d 实例
print() // Uncaught TypeError

解决方法

1
var print = d.getTime.bind(d)

容易出错的地方

容易出错的地方,函数调用,this 总是指向 window 全局变量,所以在对象的方法里如果有函数的调用的话(闭包的情况),this 是会指向 全局对象的,不会指向调用的对象,具体示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'xiaomin'
var test = {
name : 'bbt'
}
test.method = function(){
function hello(){
console.log(this.name)
}
hello()
}
// 调用
test.method() // 输出 xiaomin

如果需要将 this 指向调用的对象,可以将对象的 this 指向存储起来,通常我们使用 that 变量来做这个存储。改进之后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = 'xiaomin'
var test = {
name : 'bbt'
}
test.method = function(){
var that = this
function hello(){
console.log(that.name)
}
hello()
}
// 调用
test.method() // 输出 bbt

####闭包和引用

闭包我们可以理解成是在函数内部定义的函数

在 javascript 中,内部作用域可以访问到外部作用域的变量,但是外部作用域不能访问内部作用域,需要访问的时候,我们需要通过创建闭包,来操作内部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test(_count){
var count = _count
return {
inc:function(){
count++
},
get:function(){
return count
}
}
}
var a = test(4)
a.get()//4
a.inc()
a.get()//5

闭包中常会出错的面试题

1
2
3
4
5
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}

很多同学会觉得,上面的代码会正常输出0到9,但是实际是输出十次10。遇到这个题目,除了闭包的概念要理解清楚,你还需要知道,setTimeout 内的代码会被异步执行,代码会先执行所有的同步代码,即上面的这段代码会先将 for 循环执行,此时 i 的值为 10,console.log(i) 一直引用着全局变量的 i 所以会输出十次 10

改进代码,我们在 for 循环里创建一个闭包,把循环自增的 i 作为参数传入

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}

setTimeout && setInterval

javascript 是异步的单线程运行语言,其他代码运行的时候可能会阻塞 setTimeout && setInterval 的运行

1
2
3
4
5
6
7
console.log(1)
setTimeout(function(){
console.log(2)
}, 0);
console.log(3)
输出结果: 1,3,2 //setTimeout 被阻塞

处理阻塞的方法是将setTimeout 和 setInterval放在回调函数里执行

1
2
3
4
5
function test(){
setTimeout(function(){
console.log(2)
}, 0);
}

setTimeout 和 setInterval 被调用时会返回一个 ID 用来清除定时器

手工清除某个定时器

1
2
var id = setTimeout(foo, 1000);
clearTimeout(id);

清楚所有的定时器

1
2
3
4
5
6
7
var lastId = setTimeout(function(){
console.log('11')
}, 0);
for(var i=0;i<lastId;i++;){
clearTimeout(i);
}

获取最后一个定时器的id,遍历清除定时器,可以清除所有的定时器。

类型

####包装对象

数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。

我们可以通过构造函数,将原始类型转化为对应的对象即包装对象,从而是原始类型能够方便的调用某些方法

数值,字符串,布尔值的类型转换函数分别是 Number,String,Boolean,在调用的时候在函数前面加上New 就变成了构造函数,能够蒋对应的原始类型转化为“包装对象”

1
2
3
4
5
6
7
8
9
10
11
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

类型转换

类型转换分为强制类型转换和自动转换,javascript 是动态类型语言,在到吗解析运行时,需要的数据类型和传入的数据类型不一致的时候,javascript 会进行自动类型转化。当然,你也可以通过类型转换方法进行强制类型装换。

日常开发中,我们最常用的数据类型自动转换不过就下面三种情况

不同数据类型之间相互运算

1
'2'+4 // '24'

对非布尔值进行布尔运算

1
2
3
if('22'){
console.log('hello')
}

对非数据类型使用一元运算符

1
+'12' //12

我们也通过Number ,String,Boolean 来进行强制数据类型转换。强制类型转化的规则有点复杂,我们来了解一下。

Number 转换 引用阮老师的详细解释

1
2
3
4
5
第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。
第二步,如果 valueOf 方法返回的还是对象,则改为调用对象自身的 toString 方法。如果 toString 方法返回原始类型的值,则对该值使用 Number 函数,不再进行后续步骤。
第三步,如果 toString 方法返回的是对象,就报错。

String 转换方法同样也是通过调用原对象的 toString 方法和 valueOf 方法,但是不同的是 String 函数会先调用 toString 方法进行转换

Boolean 的转换规则会相对简单一些,除了几个特殊的值,都会被转化为 true

1
2
3
4
5
undefined
null
+0或-0
NaN
''(空字符串)

但是要注意

1
Boolean('false') //true

typeof

typeof 操作符返回数据类型,但是由于 javascript 设计的历史原因,typeof 现已经不能满足我们现在对于类型判断的要求了

Value Class Type
“foo” String string
new String(“foo”) String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function(“”) Function functio
/abc/g RegExp object (function in Nitro/V8)
new RegExp(“meow”) RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
null null object

我们可以看到,typeof 不能区分对象的数组和日期,还会把 null 判断成对象,那我们一般是什么时候用 typeof 呢。我们可以用来判断一个已经定义的变量是否被赋值。

1
2
3
4
var a
if(typeof a == 'undefined'){
console.log('a 已经被定义')
}

instanceof

instanceof 操作符通常用来判断,一个对象是否在另一个对象的原型链上,需要注意的是 instanceof 的左值是对象,右值是构造函数

1
2
3
4
5
6
7
8
9
10
11
// defining constructors
function C() {}
function D() {}
var o = new C();
// true, because: Object.getPrototypeOf(o) === C.prototype
o instanceof C;
// false, because D.prototype is nowhere in o's prototype chain
o instanceof D;

Object.prototype.toString

那么我们有没有可以用来区分变量数据类型的方法呢,有,Object.prototype.toString

一些原始数据类型也有 toString 方法,但是通常他们的 toString 方法都是改造过的,不能进行 数据类型判断,所以我们需要用 Object 原型链上的 toString 方法

1
2
3
4
var a = 1234
a.toString() // '1234'
Object.prototype.toString.call(a) // "[object Number]"

不同类型返回的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 数值 [object Number]
2. 字符串 [object String]
3.布尔值 [object Boolean]
4.undefined [object undefined]
5.null [object Null]
6.数组 [object Array]
7.arguments [object Arguments]
8.函数 [object function]
9.Error [object Error]
10.Date [object Date]
11.RegExp [object RegExp]
12.其他对象 [object object]

那么我们就能够通过 Object.prototype.toString 方法,封装一个可以判断变量数据类型的函数了

1
2
3
4
5
function type(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
type(function(){}) //"Function"

这次我们从对象、函数、类型三方面入手了解了javascript 中容易被忽视或则说比较难理解的地方,我会继续将我在学习中积累的内容分享给大家,如果大家觉得文章有需要改进或则有其他想要了解的内容的,欢迎私信,评论或则微信我,我的微信是:646321933

白霸天的博客

三亚之旅

发表于 2018-03-28 |

大家总说,身体和心灵需要有一个在路上。读书不一定所有的人都能喜欢,但是旅行绝对是每一个年龄阶段的人都不会拒绝的的一种亦或是放松亦或是长见识的方式,如果说优缺点的话,那应该就是心疼钱了。我很喜欢旅游,我觉得每年一次旅行是必要的。

旅行一般我都是临时起意的,看到哪里的机票便宜了,就买了准备走,所以我还是比较倾向于不需要办签证的地方,比如济州岛,或则国内。这次我们去了三亚。

因为是自由行,去之前做了一些粗略的攻略,确定几个自己想要游玩的景点。但是这次行程除了入住的地方和攻略是一致的,其他的都没有按照攻略来。

攻略是按照窝蜂上的推荐景点排序进行筛选的,但是到三亚的时候,当地居民推荐了我去其他景点,游客少的,消费更低一些的。

机票是在携程上买的,杭州出发到海口,海口动车到三亚(别问为啥不直飞,因为穷,哈哈哈)海口的火车站和飞机场在一个地方,掐着点的话,下飞机一个半小时就能到三亚了。担心飞机晚点问题,还有到机场需要去提行李,所以没有提前买动车票,到站买了最近一趟去三亚,所以下飞机后我们在火车站逗留了大概一个小时才出发去三亚。

D1

飞机是上午6点出发,8点55到海口,坐动车到三亚的时候已经是12点了,入住房间刚好,我们把房间定在三亚湾,190 一晚的海景房(一室一厅一厨一卫)感觉性价超级高,三亚的物价没有特别高,溜达的时候,有看到布丁酒店100多一晚的房间,但是不是在海边,具体房间是怎么样的,我也不太清楚,如果预算不够的话,可以住这种的。

因为出门就能到海边,虽然肚子有点饿,我们没有马上去觅食,而是去海边小坐了一会,没有坐太久,因为大中午的实在太热了,哈哈。在沙滩上看到了几个皮肤十分黑的男人在光着屁股晒太阳,以为是外国黑人,还在感叹他们奔放的时候发现其实是三亚本地人,脸上3条黑线

三亚湾上街购物和吃放也很方便,过一条马路就能到热闹的地方去,因为没有在淘宝上挑到自己喜欢的草帽,所以没有带,三亚很晒,所以在逛当地礼品店的时候买了一个,15块钱,和淘宝的价格差不多,没有很宰客,后面又买了一个自拍杆,20块,比淘宝上买略贵一些。

中午随便吃的重庆小面,2人吃了30多,这个比在杭州吃的时候贵一丢丢,在杭州吃可能 20多就搞定了。也说不上来,可能这个不是三亚本地的吃食吧,所以要贵一些,三亚的海鲜和热带水果还是很便宜的。

因为第一天早上为了赶飞机,3点多就起来了,我们两个都有点累,本来这一天也没有很特别的行程安排,所以我们就在酒店睡了一下午,晚上的时候本来想随意吃点,逛到了海鲜加工市场,就在市场里买了活海鲜加工着吃,2个人一共吃了300块,因为没有挑加工店,随意没有特别好吃也没有特别难吃,如果你们去的话,还是建议去第一海鲜市场,挑个开的时间长的加工店,

晚上我们吃完饭又去沙滩上坐了会,穿着拖鞋去海棠踏浪。快到10点的时候准备回房间,在沿着海滩边上的路牙上有好多大姐大妈在按摩,20块钱可以按20分钟,挺舒服的。

三亚路牙子

D2

第二天,我们睡到9点起,打算坐公交车去亚龙湾潜水,在等公交的时候有个大叔问我们去哪,可以送。开始以为是黄牛,有点不太想搭理他。他和我们说可以送我们去亚龙湾,带我们去售票点买票,他们是通过带人去售票店买票积分,然后领礼品的。他和我们说潜水的话,亚龙湾398可以潜,水上项目想多玩点的话,可以去西岛,西岛的套票是 980 可以玩18项。送我们去西岛的话只要每人5块的路费

我们想着,反正也来了,就去多玩点吧,亚龙湾是在三亚的海岸边,海水可能也会比较浑浊,出海到岛上潜水体验应该能好点。于是我们坐了他老婆的车到西岛售票处,车子沿着海边开了半个小时左右,5块一个人的路费真是良心价。

到西岛后,我们买了980 的套票,里面包括珊瑚礁潜水,保礁潜水,香蕉船,飞鱼船,快艇,观光艇,划伞,中午的自助餐,鱼疗和台风体验等小项目,也不是所有的项目都好玩,一半一半吧,反正这么买套票我觉得是十分划算的

我们上岛第一个玩的项目是潜水,潜水有潜水服,但是你还是要穿自己的泳衣在里面,玩其他水上项目也会把衣服弄湿,所以潜完之后,你可以不换衣服,直接去玩其他水上项目了。

潜水很好玩,潜水前教练会教你一些简单的手势动作,全程教练牵着你移动,所以即使不会游泳也没有关系。潜水的时候最好自己带着鱼食,吸引小鱼。虽然我没有买,但是和我一起潜的其他同学买了,蹭了一把鱼食,小鱼在自己周边游动,十分有趣,壮观

潜完水,我们衣服没换去吃了自助餐,然后去划伞,划伞这个项目说不上好玩不好玩,就是快艇拉着你在海上兜一圈,不是很刺激,看起来很好玩,实际也没有特别好玩,如果没买套票的话,不是很建议单买着玩

然后我们去坐了香蕉船,飞羽船,快艇,基本上都是你坐在气垫小船上被快艇拉着在海上兜一圈,速度比较快,挺刺激的。(香蕉船千万别坐第一排,海水很咸,十分辣眼睛,一路海水打在脸上眼睛上,全程睁不开眼睛,飞鱼船可以坐第一排,不会有很多水贱上来,第一排视野还算开阔的)

水上项目都在一个地方,我们玩了全部的水上项目基本也到下午3点钟了,去做了鱼疗足浴,其实是热带小鱼吃你的角质,脚放进去会痒痒的,但是还满舒服的。然后坐环岛电瓶去了牛岛,因为赶着要在6点前离岛,我们就再牛岛上随意逛了下,拍了几张照片。

出岛的时候人比较多,我们排队差不多排了半个小时,晚上回去的时候我问了下,打车去三亚湾要60多,不是很划算,我们去坐了公交车,在等公交的时候我买了芒果,10块钱2小袋,我数了一下,一共有11个手掌心这么大的芒果,而且十分甜,很好吃。

到酒店的时候差不多7点多,我们洗个澡出门觅食,找到一条小吃街,在解放路上,我们住的地方过去需要走15分钟,也算方便,吃了海鲜烧烤和清补凉臭豆腐,买了泡椒鸡爪,一个人50块钱管够了

吃完饭,我们照例去海滩边逛了下,躺一会,晚上的海滩真的太舒服了,让人想融化在沙滩上。等老了真心可以在海滩边买一套房子养老,哈哈哈。晚上的海滩很热闹,很多游客在踩水,拍婚纱照,还有当地的老人歌舞团的聚会活动,到处是欢声笑语。从海滩边回来,我们在路牙边做了马杀鸡,好多老阿姨按摩,20块钱可以按20分钟,不贵,很舒服,按完然后回去睡觉
三亚路牙子

D3

前一天玩水玩的比较累,第二天10点才起来,按照原来的计划,是想去南山文化旅游区,那边的108米的海上观音很出名,但是后来没有去,也幸好没去,30多度在外面逛露天的景点实在不太合适,昨天玩水一起认识的几个小伙伴去了,他们和我说让我别去,他们就逛了10几分钟就走了,因为实在太热了

在我们不知道要去哪里的时候,昨天带我们去西岛的大姐发微信和我说,我们可以去热带雨林,20块钱一个人带我们玩一天,在景点买票就行。而且雨林里有丛林飞跃,是我很想玩的项目,于是我们去了呀渃哒,大姐和我们说这个在海南话里是 1,2,3 的意思。呀渃哒 的门票加丛林飞跃项目是 300块一人,后来我们又加了踏步戏水的项目,每人150。踏步戏水不是很建议玩,感觉比较像是团建,因为大家的身体素质不一样,景区不太敢开放太冒险的路段,所以不是很刺激,没有特别好玩。丛林飞跃就是玉林滑索项目,索道从一个山头到另一个山头,一共有130米,速度不是很快,但是在玉林中飞过,眺看整个玉林的样子真是很舒服,值得一玩。可惜就是不让带手机,索道的终点会有人给你拍照,照片是要钱的,要不要照片就看你个人意愿了,反正我是不太喜欢。

丛林飞跃之后可以在玉林里闲逛一会,景区会给散客一个电子讲解器,讲解到是挺详细的,反正我是没有耐心听下来,全程拍照瞎晃。只记住槟榔树在海南是一个很重要的文化,以前小伙子要爬49颗槟榔树才能取到老婆。

雨林很舒服,有身处大自然的感觉,值得去,反正我是觉得一天雨林一天海滩的玩耍方案挺合适的,到是海滩和雨林的景点是比较多的,除了我玩的几个,你们可以自由选择下

晚上到酒店,我们又重复了前一天的活动,小吃,海滩,马杀鸡

三亚路牙子

D4

第四天,我们晚上11点的飞机,而且需要从三亚坐动车到海口机场,所以我们12点退了房子,打算继续在海滩边玩到下午5,6点再出发。不过现实总是没有想象的美好,30度的海滩,也热,我们差不多在海滩边逗留了1个小时,去吃了午饭买了些菠萝和泡面就公交到动车站避暑了。

我们没有提前买动车票,总是买的就近一般有座位的,4点从三亚触发,到海口的检完票的时候差不多8点了。逛了免税店,买了2千的面膜和洗面奶,刚好9点多。在机场吃了泡面,玩了会机场农药就开始回城机票检票了

总得来说第四天,没有很紧张的行程,就是瞎逛,买东西,吃东西。三亚的景点都离得比价远,去一个景点公交都差不多要2个小时的样子,来回需要4个小时在路上。如果要赶着玩的话其实可以把行李寄存起来再玩一个景点。但是想着太累了,我们还是把时间留下去逛免税店了。

三亚路牙子

装备

以下我罗列的非必要,但是很有用,哈哈哈

防晒 记录旅程 玩水
防晒霜 自拍杆 手机防水袋
防晒外套 美美的衣服 拖鞋
帽子 墨镜 钱

你可能想要知道的东西

  1. 我们190订的海景房,是在爱彼迎上定的,这个平台主打民宿所以比携程上便宜,平台注册链接 https://zh.airbnb.com/c/09425c
  2. 三亚水果很便宜,最便宜的是在槟榔谷,10块钱能买5个菠萝。市区0块钱只能买3个,如果想买没有削皮的菠萝,可以去就近的菜市场
  3. 马杀鸡在三亚湾的海岸上,20块1次,可以按20分钟
  4. 如果你有其他的想问,可以私我像我,我的微信:646321933

我们的行程是4天3夜,如果有时间的话,建议还是6天5夜,这样能把大部分的景点覆盖到,蜈支洲岛,分界洲,南山文化旅游区,槟榔谷,国家森林公园都可以去走一走。三亚是个很舒服的城市,在三亚,你会感觉整个时间都慢下来,很舒服很放松,你会喜欢的

白霸天的博客

web 浏览器指纹跨域共享

发表于 2018-03-05 |

概念:设备id 即设备指纹,用来表示用户设备的唯一性

背景

最近在做用户行为分析项目的开发,需要采集用户的设备信息,需要用设备指纹来唯一表示用户操作设备。web 存储都和浏览器相关,我们无法通过js 来标识一台电脑,只能以浏览器作为设备维度来采集设备信息。即用户电脑中一个浏览器就是一个设备。

问题

web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

方法

我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理

实现

SDK 采集端,调用方初始化的时候调用方法

collect.setIframe = function () {
      var that = this
      var iframe = document.createElement('iframe')
      iframe.src = "http://localhost:82/"
      iframe.style = 'display:none'
      document.body.appendChild(iframe)
      iframe.onload = function () {
        iframe.contentWindow.postMessage('loaded','*');
      }

      //监听message事件
      window.addEventListener("message", function(){
            that.deviceId = event.data.deviceId

            console.log('获取设备id',that.deviceId)

            sessionStorage.setItem('PageSessionID',helper.upid())
            helper.send(that.getParames(), that.eventUrl);
            helper.sendDevice(that.getDevice(), that.deviceUrl);
      }, false);
}

嵌套在 iframe 静态页面里的脚本

<script>

     var getDeviceId = function() {
             return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                     var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                     return v.toString(16);
             });
     };

     var hasCreated = false
     var _deviceId = ''
     if(document.cookie){
             document.cookie.split(';').forEach(function(i){
                     if(i.indexOf('deviceId')>-1){
                             hasCreated = true
                             _deviceId = i.split('=')[1]
                     }
             })
     }

     if(!_deviceId && (sessionStorage.getItem('deviceId')||localStorage.getItem('deviceId'))){
             hasCreated = true
             _deviceId = sessionStorage.getItem('deviceId')||localStorage.getItem('deviceId')
     }


     if(!hasCreated) {
             _deviceId =  getDeviceId()
             document.cookie = 'deviceId=' + _deviceId
             sessionStorage.setItem('deviceId',_deviceId)
             localStorage.setItem('deviceId',_deviceId)
     }

     //回调函数
     function receiveMessageFromIndex ( event ) {
             console.log( 'receiveMessageFromIndex', event )
             parent.postMessage( {deviceId: _deviceId}, '*');
     }
     //监听message事件
     window.addEventListener("message", receiveMessageFromIndex, false);

 </script>

扩展阅读

跨浏览器cookie
跨浏览器指纹识别
浏览器指纹追踪
使用postMessage解决iframe跨域通信问题
window.postMessage

白霸天的博客

跨域问题导致设置 cookie 不生效的问题

发表于 2018-02-26 |

我们看下跨域不生效的问题,首先抛出两个问题:

  1. 我们如何设置 cookie ?
  2. 又如何确定 cookie 设置是否生效了 ?

首先,我们实现一个简单的接口,新建一个 test.js 文件,将如下代码复制进去,通过 node test.js 启动服务,在本地就可以通过 http://localhost:3000/rest/collect/event/h5/v1/ 来访问了我们创建的接口了(node 环境安装的教程网上有很多详细的教程,本文不再赘述)

var express = require('express');
var app = express();
var URL = require('url')
var path = require('path');


app.post('/rest/collect/event/h5/v1/', function(req, res) {
        res.cookie('token','11111112222222224444444444')
        res.cookie('httpOnly-token','11111112222222224444444444',{ httpOnly: true })

        function User() {
                this.name;
                this.city;
                this.age;
        }

        var user = new User();

        if(params.id == '1') {

                user.name = "ligh";
                user.age = "1";
                user.city = "北京市";

        }else{
                user.name = "SPTING";
                user.age = "1";
                user.city = "杭州市";
        }

        var response = {status:1,data:user};
        res.send(JSON.stringify(response));
});

app.listen(3000);
console.log('Listening on port 3000...');

访问效果如下

接口访问效果如下

在前端代码中访问我们的接口
cookie设置
cookie查看
在浏览器中我们可以看到请求的 Resopnse Headers 里,有两个 set-cookie头部,区别在于一个带有 HttpOnly的标识,我们打开浏览器的调试窗口Application我们可以看到,两个数值都被设置到浏览器里了,httpOnly的值在浏览器调试窗口的http一栏,打了个小勾,说明这个变量是只能通过 http 请求来获取到这个cookie ,前端无法通过 js 的 document.cookie来获取到
就是无法操作的cookie
讲到这块内容,我们顺便讲下 cookie 设置的其他参数的作用

其他参数
cookie 和域名相关的哟,Domain 变量表示 cookie 生效的域名,expries和max-age表示 cookie 的有效时间

问题描述及解决

在开发阶段我自己用node 简单的写了一个接口,便于联调前端传参问题,希望通过 http 的set-cookie 存储变量, 但是却始终没有把 cookie 成功设置到浏览器里,经过排查发现是跨域导致的 cookie 设置不生效
cookie设置
cookie查看

不生效的原因是我本地项目启动在 http://localhost:70,但是调用的接口在 http://localhost:3000上,端口不一样,存在跨域的问题,所以虽然在 Response Header 里看到了set-cookie的操作,但是在浏览器的 application里看到,并没有被设置进来,解决办法,通过nginx 代理(最长用的跨域解决办法)

扩展

跨域的问题在开发过程中比较常见,我们经常会碰到,简单来说只要请求资源的协议,域名,端口不一致,都会导致跨域,网上的解决方法也比较多,比较成熟,本文不做扩展,附带几个链接供大家参考

跨域中的预检测请求
CORS 跨域中的 Cookie
跨域资源共享 CORS 详解
Web开发中跨域的几种解决方案

白霸天的博客

你需要知道的 nginx 基础配置

发表于 2017-05-18 |

初探nginx

今天给大家讲下nginx的基础配置,很多小伙伴在开发的过程中会使用到 nginx ,但是确对 nginx 的配置其实并不了解,今天我给大家讲下基础的配置项.nginx 的功能很多,但是说到 nginx 大家最先想到的是反向代理和负载均衡.

“负载均衡”在开发环境体现的不太明显,主要是为了解决生产环境的客户端请求很多的时候,动态的去分散给各个服务器,缓解服务器压力,充 分利用资源.而说到 “反向代理”,你的第一反应应该是”正向代理”,简称”代理”,你可以把代理想象成客户端和服务端的中介,代理的种类很多,比较常见的是,客户端对服务端进行请求的时候,代理会对请求的内容进行下载缓存,从而提高客户端的请求速度,代理还有多重代理,加密处理等功能,”反向代理”的作用和正项代理的功能相辅相成,原理也类似,反向代理将字符串和相应的服务器和端口匹配上,从而获取用户想要获取的内容,那么这两者的区分是什么呢,”正向代理”作用于客户端,”反向代理”作用于服务器.

生动的 nginx 反向代理解释

为了让大家能够更加深刻的理解代理和反向代理的模式,我们引用下知乎上车小胖的回答

有了第三方订餐外卖平台(代理),老王懒得动身前往饭店,老王打个电话或用APP,先选好某个饭店,再点好菜,外卖小哥会送上门来。由于某个品牌的饭店口碑特别好,食客络绎不绝涌入,第三方订餐电话也不绝于耳,但是限于饭店接待能力有限,无法提供及时服务,很多食客等得不耐烦了,纷纷铩羽而归,饭店老总看着煮熟的鸭子飞走了,心疼不已。痛定思痛,老总又成立了几个连锁饭店,形成一个集群,对外提供统一标准的菜品服务,电话订餐电话400-xxx-7777,当食客涌入饭店总台,总台将食客用大巴运到各个连锁店,这样食客既不需要排队,各连锁店都能高速运转起来,一举两得,老总乐开了花,并为此种运作模式起名为“反向代理”(Reverse Proxy)。

nginx 基础配置

好了,了解了 nginx 的一些概念之后,我们要开始切入今天的主题,”ngix 的基础配置”,安装好 nginx 配置之后,你对 nginx 的配置都写在 nginx.conf 的文件里,从 nginx 的配置指令作用域来讲,我们分为 5 个作用域块,分别是:

  • 全局作用域块
  • event 作用域块
  • http 指令作用域块
  • server 指令作用域块
  • location 指令作用域块

nginx基础配置

在 “全局作用域块” 作用域块中配置通用的nginx 配置,比如 nginx 的用户组信息,nginx 的并发进程数,日志存放位置等,nginx 的用户组信息配置,用来控制启动 nginx 的权限,服务并发一般情况下是越多越好,但是当超过硬件的承受范围时会适得其反,所以一般我们会配置为 auto,这样 nginx 会去检查硬件的信息,启用适当的进程数量.

日志存放位置你也可以配置在 http 作用域块,”http 作用域块”作用域通常是配置请求相关的内容,比如数据的传输,对同一个接口的请求次数上线,配置请求的潮湿时间,还有是否要对请求进行 gzip 压缩等.在之前讲 web性能优化的时候,我记得和大家讲过 gzip 压缩是提高 web 性能优化的一种手段,gzip 能对 http 请求的请求头和请求体进行压缩,从而达到优化.但是不是所有的请求都要去压缩,有些压缩之后的请求体积可能会更大,从而达不到压缩的效果.那怎么办呢,在 nginx 的配置中,你可以进行配置,设置当请求大于一定值的时候,才触发 gzip 压缩 gzip 的内容这里就深入讲解了,想要了解的同学可以去谷歌下,或则在下次 nginx 的高级配置的分享的时候我们再来细讲.

在 event 模块我们通常会配置进程的连接数量,就是每一个worker进程能并发处理(发起)的最大连接数.在 “server 指令作用域块” 我们可以进行独立项目的代理配置,

一个 http 指令中可以包含多个 server ,每一个 server 你可以看做是一个虚拟机,部署过网站的同学应该知道,若果你没有做代理,你的服务器只能部署一个项目的内容,默认是80端口,使用代理,能将客户端的请求根据端口作为区分,发散到不同的项目中.nginx 这一点,能让我们更好的利用服务器资源

在server 指令中,我们同样可以配置多个 location 指令,location 指令能将我们的字符串请求解析到对应的IP和端口,从而去获取正确的资源,location 也可以进行特殊配置,定制 网站的 404 ,500 等页面.

nginx 的项目配置示例

nginx作用域
nginx作用域
我们简单的看下,我目前使用到的两个项目的 nginx 配置,在fengdai_pc 项目中我们通过 listen 来配置项目监听端口,通过 root 来配置前端项目文件的地址,我们还通过 location 来配置了几个模块代理,account 和 funds ,定制了 404 页面,在 另一个 nginx 配置项目中我们做了https 配置,配置了域名 baibatianpc.com,并且指定了证书的位置,这样配置后,我们能通过 访问 https://baibatianpc.com/ 来查看我们的项目.这里对于如何搭建 https 环境就不细讲了,不同的开发系统证书的获取和配置方式也不太一样,想要深入了解的同学继续谷歌.

复习

好了,我们今天讲了nginx 的基础配置,我们现在来复习一下,我们今天讲的知识点有哪些

  1. nginx 常用的功能的概念,负载均衡,反向代理
  2. nginx 指令配置的5个作用域空间
  3. nginx 的每个配置作用域的基础配置项目
  4. 简单的两个项目配置示例

你都掌握了吗,联系我进行交流,WeChat:646321933

白霸天的博客

你需要知道的 webpack 配置

发表于 2017-01-09 |

好久没有写文章,最近在做项目自动化构建工具的迁移,花了一点时间去研究 webpack ,webpack 的入门其实简单,但是现有的资料比较零碎,按照我的学习路径整理了下,希望对大家能有所帮助。

接下来我将从3个部分来给大家介绍webpack,分别是 webpack 的基础配置,哪些常用的加载器,我在项目自动化构建工具改造的过程中雨大了那些问题

webpack 基础配置

首先我们需要理解四个重要的概念

  1. 入口(你需要打包的文件声明),你的项目需要什么依赖没在这里进行声明,require 你需要的依赖,webpack 会直接和间接的找到依赖文件进行打包,可传字符串,数组,对象
         // 配置了3个入口文件
         entry: [
                 './config/dependencies.js',
                 './config/index.js',
                 './config/cssImport.js'
         ]
    
  2. 出口(你打包资源后到哪个目录哪个文件),声明依赖打包后的文件输出的目录及命名方式,可传字符串,数组,对象
         //声明了依赖压缩打包之后会被添加到 build 目录的 bundle.js 文件里
         output: {
                 path: path.join(__dirname, '../build'),
                 filename: 'bundle.js',
         },
    
  3. loader(模块加载器)能将各种资源的依赖模块打包成webpack 能够理解的 js 模块,从而进行你需要的操作,如 css 预编译,图片压缩,路径转换等
         loaders: [
                     {
                         test: /\.js?$/,
                         exclude: /(node_modules|bower_components)/,
                         loader: 'babel-loader', // 'babel-loader' is also a legal name to reference
                         query: {
                                 presets: ['es2015']
                         }
                 }]
    
  4. 插件(插件用于扩展 loader 的能力)你可以用插件进行定义环境变量对代码进行打包压缩。

         //声明
         const ImageminPlugin = require("imagemin-webpack-plugin").default
         const CopyWebpackPlugin = require('copy-webpack-plugin')
    
         //从imgSrc 目录压缩图片,压缩完拷贝到 build/img 目录下
         plugins: [
                 new CopyWebpackPlugin([
                         { from: 'imgSrc' ,to:'img'}
                 ]),
                 new ImageminPlugin(
                         { test: /\.(jpe?g|png|gif|svg)$/i }
                         )
         ]
    

概念方面如果有不清晰的可以看下 webpack 的中文文档

webpack 的常用加载器

loader 用于常用的源代码进行装换,常用的有js编译,css 预编译,图片压缩等,这些都是项目中比较常见的,大家平时不需要记忆,只要能大概知道有这么一个东西,需要用到的时候去查阅就行,loader 官方收录文档

在项目迁移中遇到的问题

由于各种历史原因,我们的项目目录结构凌乱,项目依赖多,结构复杂,使用着 angular + gulp 进行开发,自动化构建工具还处于刀耕火种的年代,发版本的时候通过前端给包,不安全,不规范。新的一年我们尝试去改变这种现状,洗完通过运维直接拉去前端代码,这样能充分的保证前端代码的一致性。

目前的项目结构的控诉

  1. 项目所有问文件处于一个平级状态
  2. 有3个依赖包文件夹分别是 framwork,lib,node_modules(各种历史遗留问题)
  3. 源图片imgSrc和压缩后的img在同一个目录
  4. 人工手动打包的时候要很小心的删除不必要的文件

现有的改进方案

  1. 创建 build 文件夹,nginx 代理 到 build 目录
  2. config 中配置 webpack 构建打包任务,以及各种依赖的入口文件
  3. ib && framwork 移到 build 文件
  4. less 中对图片的引用路径需要变动,因为img 的路径变动了,tpls 也变动了,所以在视图中直接引用的不需要变化
  5. gulpfile 中的 js 压缩,less 编译,图片压缩内容迁移到 webpack 任务中
  6. 通过 npm run dev 进行编译构建
  7. 图片压缩
  8. 依赖及模块文件变化时实时构建项目
  9. 弃用 gulpfile 编译构建

创建 build 文件夹,nginx 代理 到 build

目录,配置生产环境依赖目录,运维可以直接将代理指向该目录,不需要人工手动剔除多余的目录

config 中配置 webpack 构建打包任务,以及各种依赖的入口文件

建立config 目录,存放 webpack 配置文件目录,及各种依赖声明目录,我们一共声明了3个依赖入口文件,分别是npm 管理的生产依赖文件,css 文件,项目逻辑js文件。css 和 生产依赖的内容比较烧,我们手动 require 一下,但是我们项目是已经经过近两年的迭代开发,逻辑代码 js 文件繁多,目录结构负责,所以我们需要写一个简单的 node 脚本,递归查询项目目录结构,动态 require 写入到我们的js入口文件中,node 脚本 如下

        var files = fs.readdirSync('js')
        var jsPath = 'js'

        fs.unlink('config/index.js')

        var getFileName = function (files,dirPath) {

        files.forEach(function (filename) {
                var fullname = path.join(dirPath,filename)
                var stats = fs.statSync(fullname)

                if (stats.isDirectory()){
                        var subFiles = fs.readdirSync(fullname)
                        getFileName(subFiles,fullname)
                } else {
                        let file = './../'+dirPath+'/'+filename
                        fs.writeFile('./config/index.js', 'require (\"'+file +'\")\n', {
                                flag: 'a'
                        }, function(err){
                                 if(err) throw err
                        })
                }
        })
}

getFileName(files,jsPath)

配置我们的入口和输入

        entry: [
                './config/dependencies.js',
                './config/index.js',
                './config/cssImport.js'
        ],
        output: {
                path: path.join(__dirname, '../build'),
                filename: '[name].js',
        },

然后是对我们的gulp task 进行迁移,我们需要配置 js 语法编译,css 预处理,图片压缩的功能。js 编译只需要使用 babel-loader ,通过 npm install babel-loader 安装加载器,指定匹配的文件及需要忽略的文件,指定转化语法,设定转码规则,就配置成功了

        {
            test: /\.js?$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'babel-loader', // 'babel-loader' is also a legal name to reference
            query: {
                presets: ['es2015']
            }
        }

css 预编译,我们使用了 less 做为 css 开发工具,编译的时候需要用到的 loader 比较多,通过less-loader,css-loader,style-loader的链式调用,将样式作用于DOM

        rules: [{
            test: /\.less$/,
            use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "less-loader" // compiles Less to CSS
            }]
        }]

或则

loaders: [
            {
                test: /\.less$/,
                use: ['style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            //支持@important引入css
                            importLoaders: 1
                        }
                    },
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function() {
                                return [
                                //一定要写在require("autoprefixer")前面,否则require("autoprefixer")无效
                                require('postcss-import')(),
                                require("autoprefixer")({
                                "browsers": ["Android >= 4.1", "iOS >= 7.0", "ie >= 8"]})]
                            }
                        }
                    },
                    'less-loader']
                }]

图片压缩我们选择了两个插件分别是imagemin-webpack-plugin和copy-webpack-plugin,imagemin 实现图片压缩,copy 实现图片资源拷贝,具体配置如下

//插件引用
const ImageminPlugin = require("imagemin-webpack-plugin").default
const CopyWebpackPlugin = require('copy-webpack-plugin')

//插件使用
plugins: [
                new CopyWebpackPlugin([
                        { from: 'imgSrc' ,to:'img'}
                ]),
                new ImageminPlugin(
                        { test: /\.(jpe?g|png|gif|svg)$/i }
                        )
        ]

了解完细节我们看下一个整体的配置

/**
 * Created by bailinlin on 2018/1/4.
 */
const fs = require ("fs")
const path = require ("path")

const ImageminPlugin = require("imagemin-webpack-plugin").default
const CopyWebpackPlugin = require('copy-webpack-plugin')


var files = fs.readdirSync('js')
var jsPath = 'js'

fs.unlink('config/index.js')

var getFileName = function (files,dirPath) {

        files.forEach(function (filename) {
                var fullname = path.join(dirPath,filename)
                var stats = fs.statSync(fullname)

                if (stats.isDirectory()){
                        var subFiles = fs.readdirSync(fullname)
                        getFileName(subFiles,fullname)
                } else {
                        let file = './../'+dirPath+'/'+filename
                        fs.writeFile('./config/index.js', 'require (\"'+file +'\")\n', {
                                flag: 'a'
                        }, function(err){
                                 if(err) throw err
                        })
                }
        })
}

getFileName(files,jsPath)

module.exports = {
        entry: [
                './config/dependencies.js',
                './config/index.js',
                './config/cssImport.js'
        ],
        output: {
                path: path.join(__dirname, '../build'),
                filename: 'bundle.js',
        },
        module: {
                loaders: [
                    {
                        test: /\.js?$/,
                    exclude: /(node_modules|bower_components)/,
            loader: 'babel-loader', // 'babel-loader' is also a legal name to reference
            query: {
                presets: ['es2015']
            }
                },{
                test: /\.less$/,
                use: ['style-loader',
                {
                loader: 'css-loader',
                options: {
                 //支持@important引入css
                    importLoaders: 1
                }
                },{
                loader: 'postcss-loader',
                options: {
                plugins: function() {
                    return [
                    //一定要写在require("autoprefixer")前面,否则require("autoprefixer")无效
                    require('postcss-import')(),
                    require("autoprefixer")({
                    "browsers": ["Android >= 4.1", "iOS >= 7.0", "ie >= 8"]})]
                     }
                     }
                    },
                    'less-loader']
                },{
                    test: /\.(jpe?g|png|gif|svg)$/i,
                    use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192
                        }
                    }]
                }]
        },
        plugins: [
                new CopyWebpackPlugin([
                        { from: 'imgSrc' ,to:'img'}
                ]),
                new ImageminPlugin(
                        { test: /\.(jpe?g|png|gif|svg)$/i }
                        )
        ]
}

实现热编译

实现完配置之后我们要考虑另一个问题了,我们希望修改完之后能实时编译预览,不用每次都手动的跑一边命令,我们可以选择 webpack 提供的 node 服务,webpack-dev-server,在配置文件里新增一个 webpack.server.js 的文件,在文件中 require 需要的依赖,webpack-dev-server 可以指定服务启动的目录,及服务监听的端口,我们的配置如下

/**
 * Created by bailinlin on 2018/1/4.
 */
var path = require("path")
var webpack = require("webpack")
var webpackDevServer = require("webpack-dev-server")
var webpackCfg = require("./webpack.config.js")

var compiler = webpack(webpackCfg)

//init server
var app = new webpackDevServer(compiler, {
        contentBase        : path.join(__dirname, '../build'),
        noInfo             : true,
        hot                : true,
        historyApiFallback : true,
        stats              : { colors : true },
        //注意此处publicPath必填
        publicPath: webpackCfg.output.publicPath
})

app.listen(9090, "localhost", function (err) {
        if (err) {
                console.log(err)
        }
})

console.log("listen at http://localhost:9090")

启动我们的服务

配置完成之后,我们需要启动我们的服务,你可以再你的控制台中直接输入 node config/webpack.server.js 来启动服务,webpack --config config/webpack.config.js 来进行编译构建,也可以在 package.json 文件中配置 script 如


“scripts”: {
“start”:”node config/webpack.server.js”,
“build”: “webpack –config config/webpack.config.js”
},

这样你就可以通过 npm start 和 npm run build来控制你的服务启动和项目构建

参考文章列表

如果你还想更深入理解,你可以继续阅读这些扩展文章
理解概念
配置讲解
常会用到的loaders
webpack打包原理
webpack全局变量
less 图片解析问题
其他优秀的配置文章
《使用webpack-dev-server实现热更新》
imagemin-webpack-plugin
copy-webpack-plugin

白霸天的博客

你需要知道的小程序开发基础

发表于 2016-11-01 |

微信小程序出来已有段时间,虽还在内测阶段。利用空闲时间,我把蜂贷微信项目部分迁移到小程序上。

##1.目录结构

小程序的主体由三个文件组成,这三个文件要放在项目的根目录下,分别是

  1. app.js 配置小程序的逻辑
  2. app.json 公共设置
  3. app.wxss 公共样式

    小程序可以自定义 page,但是 page 需要在 app.json 中做出声明,不然IDE会报错,找不到页面。小程序的页面由四个文件组成,分别是

  4. .js文件 页面逻辑

  5. .wxml 视图层文件,页面结构
  6. .wxss 样式文件,页面样式表
  7. .json 文件,配置文件,页面配置

2.小程序配置

app.json 决定页面文件的路径、窗口表现、设置网络超时时间、设置多少 tab 。

在 pages 对象里定义页面路径,pages 接受由字符串组成的数组,pages数组的第一个元素就是小程序的首页。

  1. window 用于设置小程序的状态栏、导航条、标题、窗口背景色。
  2. tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。
  3. networkTimeout 用来设置各种网络请求超时时间
  4. debug 是布尔类型,用来配置是否在开发者工具中开启 debug 模式

3.小程序视图

在小程序中,你不能继续用 html 中的标签来构造你的页面,MANA 框架有特定的容器组件,view,scroll-view 以及 swiper。

  1. view 是视图容器,类似于 html 中的 div ,但是不同的是,用 view 包裹的内容,在超出设备窗口的时候,它实现的效果如 css 样式设置的 overflow:hidden
  2. 如果你需要实现类似通讯录或则聊天列表的滚动效果,你需要使用 scroll-view 滚动容器组件,它实现的效果如 css 样式设置的 overflow:scroll 。
  3. swiper 是滑块视图组件,如果你要实现类似轮播图的效果的话,他是你的不二之选,你能通过属性配置来控制是否显示圆点,是否自动播放,切换时间,以及切换间隔时间等。
    小程序的MANA也实现了数据的绑定,写法类似于 Angular 和 Vue,通过双括号的形式 如: 即可,值得注意的是,如果你写在容器(为了便于描述和理解,下文会以标签来描述)于之间的话,你直接把变量写在双括号里即可,如: ,但是如果你给标签的属性绑定变量,你需要将双括号放在双引号内,如:,类似于Angular 和 Vue,你也能在双括号内进行简单的运算,如:。

细心的同学可能发现了在介绍数据绑定的时候我们用了wx:if 的属性,这是 MANA 提供的条件渲染,通过判断 wx:if 传布尔值(非布尔类型进行隐士转化)来控制是否渲染标签中的内容。在 MANA 中还有一个属性能控制内容的显隐,不同的是,wx:if 只有在为 true 的时候才回去渲染标签中的内容,而 hidden 始终会渲染内容,只是根据条件来控制内容的显示与否。

此外MANA 也为我们提供了较为实用的列表渲染,wx:for 接受一个数组,在页面中能根据数组中的值来渲染页面列表

除了使用列表渲染来复用一块视图外,你还可以通过模版来进行复用,你能在 template 中定义一块代码片段,然后在不同的页面中引用,如:

<template name="odd">
  <view> odd </view>
</template>
<template name="even">
  <view> even </view>
</template>

<block wx:for="{{[1, 2, 3, 4, 5]}}">
    <template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>

除了 template 外,MANA 还提供了另外两种方式来进行应用和复用,import 和 include ,import 有作用域的概念,他只会引用目标文件中定义的模版。include可以将目标文件除了

除MANA 同样也定义了常用的事件分类,如

  1. touchstart 手指触摸动作开始
  2. touchmove 手指触摸后移动
  3. touchcancel 手指触摸动作被打断,如来电提醒,弹窗
  4. touchend 手指触摸动作结束
  5. tap 手指触摸后马上离开
  6. longtap 手指触摸后,超过350ms再离开

    4.组件样式

定义在 app.wxss 中的样式为全局样式,作用于每一个页面。在 page 的 wxss 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.wxss 中相同的选择器。如果你写过 css ,那你就能轻松驾驭 wxss,wxss 在选择器上做了限制,目前支持的选择器有:

  1. .class 如:.intro 选择所有拥有 class=”intro” 的组件
  2. #id 如:#firstname 选择拥有 id=”firstname” 的组件
  3. element 如: view 选择所有 view 组件
  4. element, 如: element view checkbox 选择所有文档的 view 组件和所有的 checkbox 组件
  5. ::after 如:view::after 在 view 组件后边插入内容
  6. ::before 如:view::before 在 view 组件前边插入内容

开发过移动端的前端er 都知道,苹果手机有物理像素和逻辑像素的区别,比如设备的像素是350px,设计稿的像素是750px;一般在开发过程中,我们会使用自动化构建工具去做像素转化,或则是使用预处理器定义像素转化函数进行处理,在小程序的开发中,大可不必这么麻烦,小程序提供了一个 rpx 的单位,你可以直接写上你在设计稿中测量的数值即可,小程序开发工具在编译过程中会自动帮你做转换。

在下次小程序分享《小程序开发踩坑(二)》的时候,会教大家如何与后端进行数据交互,欢迎感兴趣的小伙伴订阅博客。

蜂贷微信端

白霸天的博客

使用 hexo 搭建你的 github.io 博客网站

发表于 2016-06-30 |

作为前端er,我们每天都要保持学习,前阵子有一片文章很火在 2016 年学 JavaScript 是一种什么样的体验?,前端技术日新月异,我们不仅要保持学习,还需要掌握一个好的,适合自己的学习方式.在这个信息这个发达的时代,你不缺输入的机会,却少的反而是我们的输出机会.大多数的前端没有去记录自己的所学,所用,即使有记录,也都是在一些第三方的博客网站, 这就会有这么一个情况.持续性不够,发布过的文章没有去回顾,这个博客网站发一篇,那个博客网站发一篇,而且很没归属感.

然而,github 作为我们的程序员圈子的同性交友社区,面试必看的内容,我们为什么不在 github 上搭一个自己的博客呢,在 github 上你可以直接建立一个仓库,然后在 issue 里写博客,我个人比较建议的是,搭一个属于你自己的 github pages,在 github pages 上记录你的博客. 首先是外观上比较好看,而且你可以依照你的心情改变你的博客主题,其次,你可以通过你的 github 用户名点 github.io 的方式来直接访问你的博客网站,感觉有了一个属于自己的网站一样,不要钱,还在一定程度上满足了你的虚荣心.

快速搭建

如果你是有一定开发经验的前端,你的电脑一定已经安装了 git 和 node ,那你在搭建博客之前你只要先全局安装 hexo 的命令行工具,命令如下

1
$ npm install -g hexo-cli

然后你在你的电脑里新建一个空的文件夹命名就按照 userName.github.io 的方式来命名 例如我的项目我命名为 bailinlin.github.io, 其次,你需要进入你新建的这个文件夹,执行命令

1
$ hexo init

等待一定时间,你会看到你新建的文件夹里出现了好多初始化文件,现在这个时候一个基本的简单博客搭建成功了,你运行以下命令

1
$ hexo s -p 3000

就能在 localhost:3000 中看到一个博客网站了,现在你要做的就是为你的博客网站做一些个性化的定制,打开_config.yml 文件.修改文件内的参数,

1
2
3
4
5
6
title: 白霸天的博客 //博客网站的title
subtitle: //子标题
description: 前端,白霸天,博客 //网站描述
author: bailinlin //网站作者
language: zh-Hans //网站语言,用于本地化
... // 等其它配置

发布博客

关联远程仓库

博客搭建好之后,你需要做的是把博客发布到 github 上,首先你需要在 github 上新建一个仓库,命名和你新建的文件夹一样, username.githhub.io 值得注意的是,这里的 username 必须和你的github 用户名一样,大小写也要一样.然后你需要进入你的本地文件夹,关联你的远程仓库.

首先

1
$ git init

然后

1
$ git remote add origin git@github.com:username/username.github.io.git

然后我们只要把代码推到远程仓库就行了,不过这里我们要有个约定,我们在 master 上发布博客,在 dev 分支上修改我们的博客内容和项目配置,也就是说我们发布博客后,master 分支上就是我们的博客的静态文件,dev 分支上的代码就是我们世纪维护的博客内容,如图
master 分支:
master分支
dev 分支:
master分支

安装 hexo 的一键部署命令工具

通过npm 安装

1
$ npm install hexo-deployer-git --save

修改部署配置,在 _config.yml 中添加如下配置

1
2
3
4
deploy:
type: git
branch: master
repo: git@github.com:bailinlin/bailinlin.github.io.git //此处记得改成你的 github 仓库的地址

你的 first commit

按照约定我们先切分支,通过命令把项目切换到 dev 分支

1
$ git checkout -b dev

然后 add 你的项目代码

1
$ git add .

你的 first commit

1
$ git commit -m ":tada: init blog"

然后 push 你的代码到远程

1
$ git push --set-upstream origin dev

你的第一次博客发布

发布其实很简单的,我们之前也说是一个命令行发布的,所以你只要

1
$ hexo deploy

然后你就可以看到一个属于你的 github.io 的网站了

改变你的博客网站主题

个人比较推荐的是 next ,大量的留白,让你的博客看起来简洁大方,或则你喜欢其它的主题也行,你需要在你的博客文件夹里clone 下你需要的主题

1
$ git clone https://github.com/iissnan/hexo-theme-next themes/next

然后在 _config.yml 文件夹里修改你的主题配置

1
$ theme: next

然后 hexo s 就能看到你修改的主题生效了,为了防止 clone 下来的主题文件和自己的博客文件冲突,我删除了clone 下来的 .git 文件.你再按照你的 first commit 的操作提交你的修改就行了,然后别忘记了 deploy

白霸天的博客

你需要知道的 gulp 自动化构建

发表于 2016-03-30 |

为什么要前端自动化

什么是前端自动化构建就不说了,因为我不是写书的。在前端开发实践中,大公司都会有自己的基础前端架构,能容包括了开发环境、代码管理,代码质量,性能检测,命令行工具,开发规范,开发流程,前端架构及性能优化。相对而言,小公司或则是创业型的公司,前端架构这块做得就相对没有这么好,甚至于很不规范,而规范的目的在于提升工作效率。

而规范需要一定的过程,我们就先从代码质量,代码管理上入手。

  1. 对代码(html,css,js)进行语法检查
  2. 对图片,代码进行压缩
  3. 对sass。less 的css预处理器进行编译
  4. 期望代码有改动后,能自动刷新页面
  5. …

这些操作,我们可以通过人工来完成,但是效率真的低到没朋友,难道语法检查你要自己一行一行的review,或则是拜托你的同事帮你一行一行的 review 么。如果你让我做这个,我肯定和你绝交…但是 review 的目的是帮助我们写出高质量的代码。这是必不可少的,所以我们期望能有一个自动帮我们实现代码检测压缩的工具。只要一个命令,你就能轻松的实现代码压缩,图片压缩,css预处理器编译等原来需要你去人工完成的任务,是不是爽到爆炸。

在项目自动化构建工具中,大家用得比较多的,分别是grunt,gulp。与这些自动化工具配套的包管理工具呢,通常还有npm。node包含了npm的包,所以只要你的系统里安装的 node,你就可以在你的控制台里通过 npm install 来安装你的项目依赖。还有的就是最近流行起来的 webpack 模块管理工具,大家对webpack 的反应也很好,所以我们打算在项目开发的时候把 gulp 和 webpack 一起用起来,并把研究后的搭建流程写成教程。这次分享的是gulp的搭建,下次等我的后台项目开始用 webpack 的时候,再来分享一篇。

从零开始搭建 gulp 前端自动化

  1. 安装node.js
  2. npm init 生成package文件,或则你可以自己手动生成
  3. 在控制台中输入npm install --save-dev gulp命令,在项目中安装gulp
  4. 配置gulp任务
  5. 在控制台中输入 gulp或则gulp default测试你的gulp任务
  6. 配置你真正需要的 gulp 任务,(压缩,代码质量检查,浏览器自动刷新)
var gulp = require('gulp');
gulp.task('default',function(){
    console.log("hello")
});



#####浏览器自动刷新

1. 在你的谷歌浏览器里安装插件。关键字`livereload`
2. 通过命令`mpn install gulp-livereload --save-dev`来安装依赖
3. 在gulp文件中引入`livereload = require('gulp-livereload'),`
4. 在gulp的`watch`任务中通过 `livereload.listen([options])`启动刷新服务
5. 定义的任务在最后加入一个工作流`.pipe(livereload())`,
6. 在启动后进入到这个任务后,开启谷歌插件,就能自动刷新浏览器了

#gulpfile.js 文件

    var gulp = require('gulp'),
    uglify = require('gulp-uglify'),
    livereload = require('gulp-livereload'),

gulp.task('test',function() {
    return gulp.src('js/test.js')
        .pipe(uglify())
        .pipe(gulp.dest('build'))
        .pipe(livereload())
});

gulp.task('watch',function(){
    livereload.listen();
    gulp.watch('js/test.js', ['test']);
});

当你修改你的test.js 文件之后,ctrl + s 保存,你就可以看到时时刷新。

7.代码压缩

1.通过命令`mpn install gulp-uglify --save-dev`来安装依赖(js 压缩)
2.通过命令`mpn install gulp-concat --save-dev`来安装依赖(合并压缩后的文件到一个文件)

    #gulpfile.js 文件

    uglify = require('gulp-uglify'),

    gulp.task('compress',function(){
    return gulp.src('js/servers/*.js')
        .pipe(uglify())
        .pipe(concat('all.js'))
        .pipe(gulp.dest('dist/js'))
        .pipe(livereload())
});

8.同理css压缩,生成雪碧图等task,代码质量检查,都是同样的先安装依赖,再引用,编写task

如果你想深入学习

我理想中的前端工作流
gulp 中文网
livereload
gulp-livereloadulp-livereload)

123
bailinlin

bailinlin

前端,白霸天,博客

22 日志
17 标签
© 2018 bailinlin
由 Hexo 强力驱动
主题 - NexT.Pisces