请问html5 form中 form的method属性的值设置成get可以,设置成post就出现下面错误,怎么弄

拒绝访问 | www.lovean.com | 百度云加速
请打开cookies.
此网站 (www.lovean.com) 的管理员禁止了您的访问。原因是您的访问包含了非浏览器特征(44c1b0b0d4fb43e9-ua98).
重新安装浏览器,或使用别的浏览器HTTP路由实例教程(三)—— CSRF攻击原理及其防护
1、什么是CSRF攻击
CSRF是跨站请求伪造(Cross-site request forgery)的英文缩写。关于CSRF攻击原理及其防护,可查看Github上的这个项目:,说得比较详细和透彻。
2、Laravel中如何避免CSRF攻击
Laravel框架中避免CSRF攻击很简单:Laravel自动为每个用户Session生成了一个CSRF Token,该Token可用于验证登录用户和发起请求者是否是同一人,如果不是则请求失败。
Laravel提供了一个全局帮助函数csrf_token来获取该Token值,因此只需在视提交图表单中添加如下HTML代码即可在请求中带上Token:
&input type="hidden" name="_token" value="&?php echo csrf_token(); ?&"&
该段代码等同于全局帮助函数csrf_field的输出:
&?php echo csrf_field(); ?&
在中还可以使用如下方式调用:
{!! csrf_field() !!}
我们在routes.php中定义如下代码:
Route::get('testCsrf',function(){
$csrf_field = csrf_field();
$html = &&&GET
&form method="POST" action="/testCsrf"&
{$csrf_field}
&input type="submit" value="Test"/&
Route::post('testCsrf',function(){
return 'Success!';
在浏览器中我们输入http://laravel.app:8000/testCsrf,点击“Test”按钮,浏览器输出:
则表示请求成功,否则,如果我们定义GET路由如下:
Route::get('testCsrf',function(){
$html = &&&GET
&form method="POST" action="/testCsrf"&
&input type="submit" value="Test"/&
则点击“Test”按钮,则抛出TokenMismatchException异常。
3、从CSRF验证中排除指定URL
并不是所有请求都需要避免CSRF攻击,比如去第三方API获取数据的请求。
可以通过在VerifyCsrfToken(app/Http/Middleware/VerifyCsrfToken.php)中将要排除的请求URL添加到$except属性数组中:
namespace App\Http\M
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseV
class VerifyCsrfToken extends BaseVerifier
* 指定从 CSRF 验证中排除的URL
* @var array
protected $except = [
'testCsrf'
这样我们刷新页面,再次在http://laravel.app:8000/testCsrf页面中点击“Test”按钮,则页面不会报错,正常输出如下内容:
4、X-CSRF-Token及其使用
如果使用Ajax提交POST表单,又该如何处理呢?我们可以将Token设置在meta中:
&meta name="csrf-token" content="{{ csrf_token() }}"&
然后在全局Ajax中使用这种方式设置X-CSRF-Token请求头并提交:
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
Laravel的VerifyCsrfToken中间件会检查X-CSRF-TOKEN请求头,如果该值和Session中CSRF值相等则验证通过,否则不通过。
5、X-XSRF-Token及其使用
除此之外,Laravel还会将CSRF的值保存到名为XSRF-TOKEN的Cookie中,然后在VerifyCsrfToken中间件验证该值,当然,我们不需要手动做任何操作,一些JavaScript框架如Angular会自动帮我们实现。
6、Laravel中CSRF验证原理分析
说了这么多使用方式,接下来我们来分析下源码,看看Laravel底层到底是如何避免CSRF攻击的:
1)首先Laravel开启Session时会生成一个token值并存放在Session中(Illuminate\Session\Store.php第90行start方法),对应源码如下:
public function start()
$this-&loadSession();
if (! $this-&has('_token')) {
$this-&regenerateToken();
return $this-&started =
2)然后重点分析VerifyToken中间件的handle方法,该方法中先通过isReading方法判断请求方式,如果请求方法是HEAD、GET、OPTIONS其中一种,则不做CSRF验证;
3)再通过shouldPassThrough方法判断请求路由是否在$excpet属性数组中进行了排除,如果做了排除也不做验证;
4)最后通过tokensMatch方法判断请求参数中的CSRF TOKEN值和Session中的Token值是否相等,如果相等则通过验证,否则抛出TokenMismatchException异常。
对应源码如下:
public function handle($request, Closure $next)
if ($this-&isReading($request) || $this-&shouldPassThrough($request) || $this-&tokensMatch($request)) {
return $this-&addCookieToResponse($request, $next($request));
throw new TokenMismatchE
注:tokensMatch方法首先从中获取_token参数值,如果请求中不包含该参数则获取X-CSRF-TOKEN请求头的值,如果该请求头也不存在则获取X-XSRF-TOKEN请求头的值,需要注意的是X-XSRF-TOKEN请求头的值需要调用的decrypt方法进行解密。
声明: 原创文章,未经允许,禁止转载!
这篇文章对我很有帮助
这篇文章对我很有帮助
学院君 has written
Laravel学院院长,终身学习者
积分:100052
职业:码农
城市:杭州
支持 Markdown 语法,提交之前可通过预览查看效果前言《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书介绍了该系列的两个主题:“作用域和闭包”以及“this和对象原型”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)
第一部分 作用域和闭包作用域是什么
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。 的赋值操作。 =操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声 明会被分解成两个独立的步骤:
首先, var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
接下来, a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
不成功的RHS引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。
词法作用域
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。
JavaScript 中有两个机制可以“欺骗”词法作用域: eval(..) 和 with 。 前者可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用 当作 作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都 将 导致代码运行变慢。 不要使用它们。
函数作用域和块作用域
函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会 在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。
从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。在 ES6 中引入了 let 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块 中。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开 发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。
我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前 首先 进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引 起很多危险的问题!
作用域闭包
闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人 才能够到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的 词法环境中书写代码的。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。
如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循 环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现 模块 等模式。模块有两个主要特征:
(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。
现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用 的事!
第二部分 this 和对象原型this 全面解析
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。
由 new 调用?绑定到新创建的对象。
由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
由上下文对象调用?绑定到那个上下文对象。
默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ? = Object.create(null) ,以保护全局对象。ES6中的箭头函数并不会使用四条标准的绑定规则, 而是根据当前的词法作用域来决定 this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。
JavaScript 中的对象有字面形式(比如 var a = { .. } )和构造形式(比如 var a = new Array(..) )。字面形式更常用,不过有时候构造形式可以提供更多选项。
许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。
对象就是键 / 值对的集合。可以通过 .propName 或者 [“propName”] 语法来获取属性值。访 问属性时, 引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]] ), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链(参见第 5 章)。
属性的特性可以通过属性描述符来控制,比如 writable 和 configurable 。此外,可以使用 Object.preventExtensions(..) 、 Object.seal(..) 和 Object.freeze(..) 来设置对象(及其 属性)的不可变性级别。
属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是 可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。
你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象, 等等)中的值, for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。
混合对象”类”
类是一种设计模式。 许多语言提供了对于面向类软件设计的原生语法。 JavaScript 也有类 似的语法,但是和其他语言中的类完全不同。
类意味着复制。
传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。
多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。
JavaScript 并不会(像类那样)自动创建对象的副本。
混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态( OtherObj.methodName.call(this, …) ),这会让代码更加难 懂并且难以维护。
此外, 显式混入实际上无法完全模拟类的复制行为, 因为对象(和函数!别忘了函数也 是对象)只能复制引用, 无法复制被引用的对象或者函数本身。 忽视这一点会导致许多 问题。
总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。
如果要访问对象中并不存在的一个属性, [[Get]] 操作(参见第 3 章)就会查找对象内部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
所有普通对象都有内置的 Object.prototype ,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。 toString() 、 valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。
关联两个对象最常用的方法是使用 new 关键词进行函数调用, 在调用的 章)中会创建一个关联其他对象的新对象。4个步骤(第2章)中会创建一个关联其他对象的新对象。
使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的 类构造函数 不一样。
JavaScript 是 中的机制有一个核心区别, 那就是不会进行复制, 对象之间是通过内部的
虽然这些 机制和传统面向类语言中的“类初始化”和“类继承”很相似, 但是
javascript 机制和传统面向对象类语言中的“类初始化”和“类继承”很相似但是 javascript 中的机制有一个核心区别,就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。
出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的 真实 机制(不仅仅是限制我们的思维模式)。
相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是 复制 而是委托。
在软件架构中你可以 选择是否 使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式: 行为委托 。
行为委托认为对象之间是兄弟关系, 互相委托, 而不是父类和子类的关系。 JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。
对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把 它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。
扩展思维导图能比较清晰的还原整本书的知识结构体系,如果你还没用看过这本书,可以按照这个思维导图的思路快速预习一遍,提高学习效率。学习新事物总容易遗忘,我比较喜欢在看书的时候用思维导图做些记录,便于自己后期复习,如果你已经看过了这本书,也建议你收藏复习。如果你有神马建议或则想法,欢迎留言或加我微信交流:
前言埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。
现有埋点三大类型
用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
手动埋点 手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
可视化埋点 可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
无埋点 无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。
我们暂时放弃可视化埋点的实现,在 手动埋点 和 无埋点 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。
思考几个问题
埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
我们要采集什么内容,进行哪些采集接口的约定
业务方通过什么方式来调用我们的采集脚本
手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
用户标识:游客用户和登录用户的采集数据怎么进行区分关联
设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
混合应用:app 与 h5 的混合应用我们要怎么进行通讯
我们要采集什么内容,进行哪些采集接口的约定第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定
"header":{ // HTTP 头部
"X-Device-Id":" 550ed4-a716-", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle":
"埋点测试页",
//页面标题,直观看到用户访问页面
"CurrentTime":
//事件发生的时间
"ExtraInfo":
//扩展字段,对具体业务分析的传参
以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。
"header":{ // HTTP 头部
"X-Device-Id"
:"550ed4-a716-"
"body":{ // HTTP Body体
"DeviceType":
//设备类型
"ScreenWide"
"ScreenHigh":
"Language":
业务方通过什么方式来调用我们的采集脚本埋点应该让调用的业务方,尽可能少有工作量,最好是什么都不用做,?,但是实现起来有点难额。我们采用的方案是让业务方在代码里通过 script 脚本来引用我们的 SDK ,业务方只要配置一些需要的参数进行埋点定制(?我们讲到过的无埋点的流量控制),然后什么都不做就可以进行基础数据的采集。
(function() {
var collect = document.createElement('script');
collect.type = 'text/javascript';
collect.async =
collect.src =
'http://collect.trc.com/index.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(collect, s);
//用户自定义要进行无埋点采集的元素,如果不进行无埋点采集,可以不配置
var _XT = [];
_XT.push(['Target','div']);
手动埋点:SDK如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集
//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})
游客与用户关联我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联
web 设备Id用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹
我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)
iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId
if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
iframe 与 SDK 通讯
function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data =
deviceId: _deviceId,
type:event.data
event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
window.attachEvent("onmessage", receiveMessageFromIndex, false)
如果你想知道可以看我的另一篇博客
单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。
window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了
// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法
collect = {}
collect.onPushStateCallback : function(){}
// 自定义的采集方法
(function(history){
var replaceState = history.replaceS
// 存储原生 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);
这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客
混合应用:app 与 h5 的混合应用我们要怎么进行通讯
现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。
纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器
// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {
collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。
我们需要暴露给业务方调用的方法
我们需要处理的事件类型
SDK 的基本实现思路
我们来看下几个核心代码的实现工具方法我们定义了几个工具方法,提高开发的幸福指数 ?
var helper = {};
// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}
// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}
//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}
//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}
//将相对路径解析成文档全路径
helper.normalize = function(){}
var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
//获取埋点配置
collect.setParames = function(){}
//更新访问路径及页面信息
collect.updatePageInfo = function(){}
//获取事件参数
collect.getParames = function(){}
//获取设备信息
collect.getDevice = function(){}
//事件采集
collect.send = function(){}
//设备采集
collect.sendDevice = function(){}
//判断才否采集,埋点采集的开关
collect.isupload = function(){
1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
//点击事件处理函数
collect.clickHandler = function(){}
//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}
//页面回退事件处理函数
collect.onPopStateHandler = function(){}
//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}
//获取记录开始加载数据信息
collect.getBeforeload = function(){}
//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){
1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}
//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}
//采集自定义事件类型
collect.dispatch = function(){}
//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}
//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}
//页面初始化调用方法
collect.init = function(){
1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法
collect.init(); // 初始化
//暴露给业务方调用的方法
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
代码的篇幅比较长,就不放在博客里了,感兴趣的同学可以在
上看完整的项目。如果你有什么不懂的或者想和我交流的,欢迎在文章下面留言联系我
前言使用过 vue 进行项目开发的同学,一定知道或者使用过 vue-cli 脚手架,他能够很好的搭建项目结构和工程,让我们能够把足够的精力放在业务开发上。也正是因为这样,很多时候我们会因为项目工期短等原因来不及或则不会刻意去了解项目工程配置,我们今天不去介绍脚手架的使用,我们去了解下脚手架为我们创建好的打包工程是怎么做的。
├── build --------------------------------- webpack相关配置文件
├── build.js --------------------------webpack打包配置文件
├── check-versions.js ------------------------------ 检查npm,nodejs版本
├── logo.png ---------------------------------- 项目 logo
├── utils.js --------------------------------------- 配置资源路径,配置css加载器
├── vue-loader.conf.js ----------------------------- 配置css加载器等
├── webpack.base.conf.js --------------------------- webpack基本配置
├── webpack.dev.conf.js ---------------------------- 用于开发的webpack设置
├── webpack.prod.conf.js --------------------------- 用于打包的webpack设置
├── config ---------------------------------- 配置文件
├── index.js ------------------------------ 开发和生产环境配置文件
├── node_modules ---------------------------- 存放依赖的目录
├── src ------------------------------------- 源码
├── assets ------------------------------ 静态文件
├── components -------------------------- 组件
├── main.js ----------------------------- 主js
├── App.vue ----------------------------- 项目入口组件
├── router ------------------------------ 路由
├── package.json ---------------------------- node配置文件
├── .babelrc--------------------------------- babel配置文件
├── .editorconfig---------------------------- 编辑器配置
├── .gitignore------------------------------- 配置git可忽略的文件
webpack配置划重点
在看项目配置文件之前,我们先了解下 webpack 几个常用的工具和插件,如果你已经十分熟悉,你可以跳过这一小节,直接去看,配置文件解析
1. path模块path 是 node.js 中的一个模块,用于处理目录的对象,提高开发效
常用方法:
path.join(): 用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix 系统是 ”/“,Windows系统是 ”\“
path.resolve() 用于将相对路径转为绝对路径
常使用的文件路径
__dirname: 总是返回被执行的 js 所在文件夹的绝对路径
__filename: 总是返回被执行的 js 的绝对路径
process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径
2.processprocess对象是Node的一个全局对象,提供当前Node进程的信息。
process 对象提供一系列属性,用于返回系统信息
process.argv:返回当前进程的命令行参数数组。
process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME
process.pid:当前进程的进程号
3.Source map简单说,就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,debug 工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL
source-map
生成一个 SourceMap 文件.
hidden-source-map
和 source-map 一样,但不会在 bundle 末尾追加注释.
inline-source-map
生成一个 DataUrl 形式的 SourceMap 文件.
eval-source-map
每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap .
cheap-source-map
生成一个没有列信息(column-mappings)的 SourceMaps 文件,不包含 loader 的 sourcemap(譬如 babel 的 sourcemap)
cheap-module-source-map
生成一个没有列信息(column-mappings)的 SourceMaps 文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。
4. webpack-merge开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。通用的配置部分,我们抽象出一个公共文件,通过
工具的“通用”配置,我们不必在环境特定的配置中重复代码。
5. ExtractTextWebpackPlugin 插件通常用来做样式文件的分离,被分离的文件不会被内嵌到
JS bundle 中,而会被放到一个单独的文件中,在样式文件比较大的时候,能够提前样式的加载,配置示例如下
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
plugins: [
new ExtractTextPlugin("styles.css"),
它会将所有的入口 chunk(entry chunks)中引用的 *.css,移动到独立分离的 CSS 文件。因此,你的样式将不再内嵌到 JS bundle 中,而是会放到一个单独的 CSS 文件(即 styles.css)当中。 如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。
6.html-webpack-plugin如果你有多个 webpack 入口点, 他们都会在生成的HTML文件中的 script 标签内。如果你有任何 CSS assets 在 webpack 的输出中(例如, 利用ExtractTextPlugin提取CSS), 那么这些将被包含在HTML head中的标签内。通常在开发中,我们为了避免 CDN 和浏览器的缓存通常会个输出文件 bundle.js 加上一个hash 值例如 [hash].bundle.js,使用
能够在创建新的 html 文件的时候将我们把带有哈希值的 bundle.js 引用到 html 文件.
7.optimize-css-assets-webpack-plugin用来优化从脚本里提炼出来的 css ,配置示例如下
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
plugins: [
new ExtractTextPlugin('styles.css'),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: { removeAll: true } },
canPrint: true
8.CopyWebpackPlugin从插件名称上我们不难看出他的作用,通常用来拷贝资源,对项目文件进行归类整合
9.friendly-errors-webpack-plugin能够更好在终端看到webapck运行的警告和错误,提高开发体验
10.UglifyjsWebpackPlugin用来压缩 js 代码
11.开发中 Server(DevServer)webpack 项目服务,我们通常会在开发阶段用来配置项目的热刷新,服务压缩,项目代理等,常用的几个配置参数介绍如下
const config = require('../config')
// config 文件里做了用户自定的服务参数配置
devServer: {
clientLogLevel: 'warning',
// 在开发攻击的控制台中显示信息,便于开发调试,你可以将参数配置成 "none" 来进行关闭
historyApiFallback: { // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
hot: true,
//启用项目的热刷新,即模块热替换特性
contentBase: false,
// 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。这里禁用,因为配置了 CopyWebpackPlugin 的使用
compress: true,
host: HOST || config.dev.host,
//指定使用一个域名。默认是 localhost
port: PORT || config.dev.port,
//指定要监听请求的端口号:
open: config.dev.autoOpenBrowser, //open 参数配置,如果配置成 true ,项目启动后会自动打开浏览器
overlay: config.dev.errorOverlay
//当有错误或则警告的时候在页面上显示一个全屏的遮罩提示
? { warnings: false, errors: true }
publicPath: config.dev.assetsPublicPath, //此路径下的打包文件可在浏览器中访问
proxy: config.dev.proxyTable,
//代理API的请求
quiet: true,
//启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台,特别是使用了 FriendlyErrorsPlugin 插件的时候
watchOptions: {
//与监视文件相关的控制选项。是否使用轮询
poll: config.dev.poll,
配置文件解析
通过了解了上面的配置,我们应该对 webpack 的常用插件和工具有了一定了解,我们来看下 vue-cli 脚手架给我们生成的配置情况
'use strict'
const path = require('path') // 引用项目的 path 模块
module.exports = {
// 路径配置
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// 各种开发服务配置
host: 'localhost', // 开发环境域名 可以被 node 全局变量process.env.HOST 重写
port: 8080, //配置开发服务端口,可以被 node 全局变量 process.env.PORT 重写, 需要使用未被占用的端口
autoOpenBrowser: false, //服务启动是否自动代开浏览器
errorOverlay: true,
//是否在发生错误的时候,在页面整屏增加一个错误遮罩
notifyOnErrors: true,
//是否通知错误 ,在我们的项目配置中和 friendly-errors-webpack-plugin 结合使用
poll: false, // 服务监听是否轮询操作
// 配饰是否使用 Eslint Loader 进行语法检测
// 如果使用,在开发构建阶段,会对你的代码会进行检测
// 检测出来的警告和错误会白展示在开发工具的控制台
useEslint: true,
//进行语法检测
// 配置是否将 eslint 语法检测的警告和错误展示在页面整屏的遮罩上
showEslintErrorsInOverlay: false,
// 语法检测的警告和错误不展示在遮罩上
* Source Maps
// https://webpack.js.org/configuration/devtool/#development
// 在上面的介绍中,我们知道 source map 是用来将我们构建后被转化的代码对应构建前的代码,便于 debug
// cheap-module-eval-source-map 和我们介绍的 cheap-module-source-map 很类似,但是 SourceMap 会被作为数据添加到包中
devtool: 'cheap-module-eval-source-map',
// 如果你的开发工具不能进行 vue-files 的 debug ,可以将以下设置设置成 false
cacheBusting: true,
cssSourceMap: true
// index.html 文件模板
index: path.resolve(__dirname, '../dist/index.html'),
// 打包路径配置
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
* Source Maps
//生产环境 source map 配置
productionSourceMap: true,
devtool: '#source-map',
// 因为很多的主流服务都会 通过 gzip 压缩过你的所有静态资源,我们的配置默认不开启 gzip
// 如果要设置成开启,请先确保已经安装好 compression-webpack-plugin 插件
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// 启动 build 命令的时候,额外添加一个参数,打包后会自动生成一个分析报告文件,例如 npm run build --report ,可以通过配置 true ,false 来关闭
bundleAnalyzerReport: process.env.npm_config_report
check-versions.js这个文件主要是用来检测当前环境中的node和npm版本和我们需要的是否一致的。
'use strict'
const chalk = require('chalk')
// 改变命令行中的字体颜色,大致这样用chalk.blue('Hello world')
const semver = require('semver')
//是用来对特定的版本号做判断的
const packageConfig = require('../package.json')
// 项目 npm 配置文件,获取依赖及版本信息,requrie返回的就是json对象
const shell = require('shelljs') //用来执行Unix系统命令,调用系统命令更加方便
//把cmd这个参数传递的值转化成前后没有空格的字符串,也就是版本号
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
const versionRequirements = [
name: 'node',
currentVersion: semver.clean(process.version),
// 提取进程版本信息转化成规定格式,也就是 '
' -> '1.2.3' 这种功能
versionRequirement: packageConfig.engines.node // package.json 的 node 的版本信息
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
//当前的版本信息
versionRequirement: packageConfig.engines.npm //package.json 的 node 的版本信息
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements. i++) {
const mod = versionRequirements[i]
// 如果当前版本号不符合 package.json 要求的版本号,红色表示当前版本信息,绿色表示要求的版本信息,添加到 warnings 待输出
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
//输出版本号不相符的提示 warnings
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings. i++) {
const warning = warnings[i]
console.log('
' + warning)
console.log()
process.exit(1)
'use strict'
//打包前判断当先开发环境的 node 和 npm 版本和 package.json 要求的时候一样
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
// 在用户打包的时候能够让用户知道正在进行,一个加载中的样式,转啊转
const rm = require('rimraf') //这个模块是用来清除之前的打的包,因为在vue-cli中每次打包会生成不同的hash
const path = require('path') //node 路径模块,便于我们操作文件路径
const chalk = require('chalk') //带颜色的输出模块,能在控制台中输出不同的样色
const webpack = require('webpack') //webpack 不解释
const config = require('../config') // 项目中的配置文件,?上面已经进行了配置介绍
const webpackConfig = require('./webpack.prod.conf') // 生产环境的配置文件
const spinner = ora('building for production...')// 实例一个打包加载中实例
spinner.start() //开始转圈,营造一个正在打包的场景
// 删除上一次打包的文件,删除成功,开始按照生产环境配置进行打包
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
//开始打包,打包结束停止 spinner 转圈,有报错则在控制台输出
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
// node 环境里的输出配置,process.stdout.write 你可以理解成 js 里的 console
process.stdout.write(stats.toString({
colors: true, //让打包的时候有颜色。
modules: false,
//去掉内置模块信息
children: false, // 去掉子模块,如果你使用了 ts-loader,设置成 true 会在打包构建阶段展示错误信息
chunks: false, // 增加包信息(设置为 false 能允许较少的冗长输出)
chunkModules: false //去除包里内置模块的信息
}) + '\n\n')
//打包出错在控制台输出 Build failed with errors ,退出打包程序
if (stats.hasErrors()) {
console.log(chalk.red('
Build failed with errors.\n'))
process.exit(1)
//打包成功则输出 Build complete 结束打包
console.log(chalk.cyan('
Build complete.\n'))
console.log(chalk.yellow(
Tip: built files are meant to be served over an HTTP server.\n' +
Opening index.html over file:// won\'t work.\n'
webpack.base.conf.js
'use strict'
const path = require('path')
// node 路径模块
const utils = require('./utils') //node 内部常用的工具类,其中包括:格式化字符串、对象的序列化、实现对象继承等常用方法
const config = require('../config') //?上面我们介绍的,项目配置文件
const vueLoaderConfig = require('./vue-loader.conf') //? 上面我们介绍的 vue 加载器配置文件
//返回当前配置文件位置是 build ,该方法放回 build/../dir 的相对路基
function resolve (dir) {
return path.join(__dirname, '..', dir)
// eslint 语法检测配置
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
// webpack 通用配置内容
module.exports = {
context: path.resolve(__dirname, '../'),
// 上下文,基础目录,用于从配置中解析入口起点和 loader
app: './src/main.js'
//起点或是应用程序的起点入口。从这个起点开始,应用程序启动执行。如果传递一个数组,那么数组的每一项都会执行。
path: config.build.assetsRoot,
//输出 bundle 的路径
filename: '[name].js',
//输出 bundle 的名称
publicPath: process.env.NODE_ENV === 'production' // 指定资源文件引用的目录,例如图片
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
resolve: {
extensions: ['.js', '.vue', '.json'], //配置模块如何解析,
// 创建应用的别名,
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
//判断配置中是否要是用 eslint 语法检测,如果使用,就将 createLintingRule 配置对象返回
...(config.dev.useEslint ? [createLintingRule()] : []),
//?是一些比较常用的加载器,及配置,不做详细介绍了
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
test: /\.(css | scss)$/,
loader: 'style-loader!css-loader!!sass-loader'
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
//防止因为 vue 资源本身就自带的 无用的 node 注入,浏览器兼容处理
setImmediate: false,
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
webpack.dev.conf.js
'use strict'
const utils = require('./utils')
//node 工具模块
const webpack = require('webpack') //webpack 不解释
const config = require('../config')//?提到的配置文件
const merge = require('webpack-merge') // merge 工具,用来合并生产和开发环境通用的基础 webpack 配置
const path = require('path')
//node 的路径模块
const baseWebpackConfig = require('./webpack.base.conf') //生产和开发环境通用的基础 webpack 配置
const CopyWebpackPlugin = require('copy-webpack-plugin') //拷贝插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
//动态生成 html 插件
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') //友好的错误输出插件
const portfinder = require('portfinder') //能够获取一个可用的随机端口号
const HOST = process.env.HOST
//node 全局环境变量的主机
const PORT = process.env.PORT && Number(process.env.PORT)
//node 全局环境变量的端口
//合并基础配置加载器的配置部分
const devWebpackConfig = merge(baseWebpackConfig, {
// 为 .vue 文件意外的独立样式文件配置加载器
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
// cheap-module-eval-source-map 在开发环境中很快
devtool: config.dev.devtool,
// 开发服务配置,? 已经细讲过,顺便回顾一下
devServer: {
clientLogLevel: 'warning',
// 在开发攻击的控制台中显示信息,便于开发调试,你可以将参数配置成 "none" 来进行关闭
historyApiFallback: { // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
hot: true,
//启用项目的热刷新,即模块热替换特性
contentBase: false,
// 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。这里禁用,因为配置了 CopyWebpackPlugin 的使用
compress: true,
host: HOST || config.dev.host,
//指定使用一个域名。默认是 localhost
port: PORT || config.dev.port,
//指定要监听请求的端口号:
open: config.dev.autoOpenBrowser, //open 参数配置,如果配置成 true ,项目启动后会自动打开浏览器
overlay: config.dev.errorOverlay
//当有错误或则警告的时候在页面上显示一个全屏的遮罩提示
? { warnings: false, errors: true }
publicPath: config.dev.assetsPublicPath, //此路径下的打包文件可在浏览器中访问
proxy: config.dev.proxyTable,
//代理API的请求
quiet: true,
//启用 quiet 后,除了初始启动信息之外的任何内容都不会被打印到控制台,特别是使用了 FriendlyErrorsPlugin 插件的时候
watchOptions: {
//与监视文件相关的控制选项。是否使用轮询
poll: config.dev.poll,
plugins: [
// DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
new webpack.HotModuleReplacementPlugin(), //启用热替换模块(Hot Module Replacement),也被称为 HMR
new webpack.NamedModulesPlugin(), // 当开启 HMR 的时候使用该插件会显示模块的相对路径,建议用于开发环境
new webpack.NoEmitOnErrorsPlugin(), 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段
//HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
// 拷贝自定义的静态资源文件
new CopyWebpackPlugin([
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
// 实例一个异步对象,执行 devWebpackConfig 配置编译
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
//设置基础端口
portfinder.getPort((err, port) => {获取端口,输出构建新
if (err) {
reject(err)
// 如果进行 e2e 测试,需要发布新端口
process.env.PORT = port
// 更新 devServer 的端口
devWebpackConfig.devServer.port = port
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
//执行打包配置文件
resolve(devWebpackConfig)
webpack.prod.conf.js
'use strict'
const path = require('path') // node 路径模块
const utils = require('./utils') //小工具函数
const webpack = require('webpack') // webpack 不解释
const config = require('../config')//?提到的配置文件
const merge = require('webpack-merge') // merge 工具,用来合并生产和开发环境通用的基础 webpack 配置
const baseWebpackConfig = require('./webpack.base.conf')//产和开发环境通用的基础 webpack 配置
const CopyWebpackPlugin = require('copy-webpack-plugin') //拷贝插件
const HtmlWebpackPlugin = require('html-webpack-plugin')
//动态生成 html 插件
const ExtractTextPlugin = require('extract-text-webpack-plugin')//用来做文件分离的插件
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')//优化提炼出来的css
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')// 压缩 js 文件插件
//生产环境配置
const env = require('../config/prod.env')
//合并基础配置加载器的配置部分
const webpackConfig = merge(baseWebpackConfig, {
//为独立分离出来的样式配置加载器和source,map
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
//配置线上的 source map 便于排查问题
devtool: config.build.productionSourceMap ? config.build.devtool : false,
//配置输出,路径,文件名
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
plugins: [
// DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用
new webpack.DefinePlugin({
'process.env': env
// 使用 UglifyJsPlugin 插件对 js 进行压缩
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
//配置插件的source map
sourceMap: config.build.productionSourceMap,
parallel: true
// 提取 css 到单独的文件,分离文件异步加载,提高加载速度
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
//如果把 allChunks 参数设置陈 false ,就不会把css 从代码块中分离出来
//代码块加载的时候 css 会被 styles-loader 动态的加载
allChunks: true,
//使用这个插件,从不同的组件中复制脱离出来,进行 css 压缩
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
//自动生成 html 文件,通常 index.html 文件都会带一个哈希值来清除缓存
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
chunksSortMode: 'dependency'
//该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 渲染模块没有变化的时候,id 不会变。
new webpack.HashedModuleIdsPlugin(),
// 提升或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。
new webpack.optimize.ModuleConcatenationPlugin(),
// 分离渲染的js 到独立的文件中
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
//被引用到的包会从 node_modules 中提取出来
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
// 拷贝自定义的静态资源文件
new CopyWebpackPlugin([
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
//判断如果配置了生产环境压缩,是则使用插件进行压缩
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
config.build.productionGzipExtensions.join('|') +
threshold: 10240,
minRatio: 0.8
//是否要生成代码打包分析报告
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
module.exports = webpackConfig
扩展?这篇文章详细的介绍了脚手架项目的 webpack 配置,但是只是 webpack 的一部分,还有很多内容值得我们去探究,如果你还感兴趣,可以阅读下面这些文章。也欢迎随时与我进行交流,微信号:
前言最近开发的埋点项目,需要记录用户行为轨迹即用户页面访问顺序。需要在页面跳转的时候,记录用户访问的信息(比如 url ,请求头部等),非单页面应用可以给 window 对象加上一个 beforeunload 事件,在页面离开时触发采集开关,但是现在很多业务是单页面应用,用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。所以单页面应用的路由插件一定运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。
pushState 和 replaceState 了解一下
history 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录
history 对象的详细信息已经有很多很好很详细的介绍文献,这里不再做总结,我们引用阮老师的教程介绍,
history.pushStatehistory.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.replaceStatehistory.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.replaceS
// 存储原生 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 的实现,我们打开 ,把项目克隆下来,或则直接在 github 上预览,在 Vue 开发的项目里,我们通过 router.push(‘home’) 来实现页面的跳转,所以我们检索下,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)
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
case 'abstract':
this.history = new AbstractHistory(this, options.base)
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
if (replace) {
history.replaceState({ key: _key }, '', url)
_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 的应用,你应该对这个两个方法能有一个比较深刻的印象,如果想要了解更多,你可以参考以下链接}

我要回帖

更多关于 form表单的method属性 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信