litanda

litanda 查看完整档案

北京编辑山东财经大学  |  信息管理与信息系统 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

litanda 赞了回答 · 2019-12-20

homebrew 无法安装,提示不能在根目录下使用

sudo chown -R $(whoami) /usr/local

/use/local的owner換成自己,就有write權限了

whoami就是一個命令,會echo當前登錄用戶的名字。當然你知道自己的用戶名,比如jack,就直接

sudo chown -R jack /usr/local

也可以

修改過權限之後,就直接跑

brew install node

不需要再用sudo,因為當前用戶已經有權限寫入/usr/local了
或者你用

ls -ld /usr/local

確認一下這個目錄的owner是否已改到自己

关注 9 回答 7

litanda 发布了文章 · 2019-12-13

call/apply的第一个参数传null时,有什么用?

最近在看 axios 的源码,里面有一些简写,比如:

utils.forEach(fns, function transform(fn) {
  data = fn(data, headers);
});

好奇心驱使我又去看了下 forEach 的封装,如下:

function forEach(obj, fn) {
  // 循环变量为空时,直接return
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // 如果 obj 不可遍历,则强制转为数组
  if (typeof obj !== 'object') {
    obj = [obj];
  }

  if (isArray(obj)) {
    // 遍历数组
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 遍历对象
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

仔细看,遍历里面其实就是执行从外面传入的 fn,不必要使用 call 函数,而且第一个参数传的是 null,那它有什么用呢?试着在控制台执行以下代码:

function forEach(obj, fn) {
  for (var i = 0, l = obj.length; i < l; i++) {
    fn.call(null, obj[i], i, obj);
  }
}
var text = '示例文字';
forEach(['a','b','c'],function({console.log(this.text)})

BD2575DA-D098-4386-B322-7C244A7D28CD.png
嗯能看到 call 第一个参数传 null 时,this 指向了 window

结论:call/apply 的第一个参数为 null 时,this 是 js 执行环境的全局变量,浏览器中是 window,其他环境(如node)是 global

axiosutils 中封装的各种数据类型的判断好全啊哈哈哈,附上地址:https://github.com/axios/axios/blob/master/lib/utils.js,面试的时候太爱问了~~

查看原文

赞 0 收藏 0 评论 0

litanda 发布了文章 · 2019-12-11

vue-router变化,页面不刷新

现象

在页面上操作后,地址的 queryexecute_status 需要变化,在代码中使用 this.$router.push() 之后,页面没有刷新,脚本没有执行。
40B4296D-4B0F-427B-A432-24B172CB130A.png

原因

两个地址使用的是同一个组件,比起销毁原组件后创建新组件,复用组件的效率更高,所以页面上没有更新。

解决

第一种方案

监听路由$route变化

  watch: {
    '$route' (to, from) {
      // 对路由变化作出响应...
    }
  }

第二种方案

利用路由的钩子函数:beforeRouteUpdate

  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }

详见官网:https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E5%93%8D%E5%BA%94%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0%E7%9A%84%E5%8F%98%E5%8C%96

查看原文

赞 0 收藏 0 评论 0

litanda 关注了专栏 · 2019-10-30

前端小而全的知识归纳

vue,react,小程序,php,node乱炖

关注 1242

litanda 收藏了文章 · 2019-10-30

推荐几个博客

经常回去一些博客网站,看看别人是怎么努力的,怎么成长的,想想自己,总会有很多收获的。

然后就会憧憬很多,想做很多事情。之后,就会发现,有好多想法,但是,就是没有时间做。我喜欢生活,喜欢阳光,喜欢漫步,喜欢代码给他人和自己创造的好玩的东西。

得了,正题,分享几个博客网吧,都是精华。

啥都不说,道哥第一个。道哥凶悍,多少人仰望。《白帽子讲web安全》

http://taosay.net/

华哥第二个。华哥神奇人物,其实,华哥很认真的,每天都在学习。小渣我只能仰望+努力。

github网址:https://github.com/firefoxbug

官方网站:http://www.firefoxbug.net/

csdn博客网:http://blog.csdn.net/firefoxbug

(总之,华哥各种网站,多的要命,官网都有了。。)

磊哥吧,磊哥很厉害啊,就是没有女盆友,唉,我要帮他介绍。

http://www.zooboa.com/

贾哥吧,贾哥满脸痘。。。人很好啊。开了个偷吃网,记得多多关注哈。

http://www.appad.net/

http://www.touchi.cc/

廖师兄吧,杭电助手的开发者,现在都出名了都。唉,仰望。

http://www.liaoshixiong.cn/

何少吧,安协会长,也是阿里的人啊。。

http://nidongdehehe.diandian.com/

鑫哥吧,写了好多技术博客啊,还有,学霸啊,怎么忍!!!!!

技术博客:http://miibotree.com/

生活博客:http://miibotree.diandian.com/


上面几个都是大三大四有成果的人啊。压力略大。

===========================================================

大耳朵,图图吧,唉,又是厉害人物

http://tinple.me/

思思学姐吧,卧槽这个人淫荡的啊,丧尸体啊。

http://veightz.diandian.com/

女王,协会女神!!!!!

http://gina1993.diandian.com/

梁翰雷吧,弗雷德君。

http://daryll.diandian.com/

焦少

http://foglunay.diandian.com/

很厉害的小贱贱。。。

http://lightlesshome.diandian.com/


上面都是协会的一些人的博客吧,值得借鉴,学习。

===============================================================

来几个其他人的博客吧。

wangyan

http://wangyan.org/blog/git-branch.html

很赞的一个人,homeschool,翁天信

http://www.dandyweng.com/

雨夜带刀

http://stylechen.com/

代码回音

http://www.codecho.com/

阮一峰

http://www.ruanyifeng.com/home.html

这个还不错

http://luckyyang.github.io/

zyzhang

http://zyzhang.github.io/blog/2012/08/29/blogging-like-a-geek/

应该也是协会人,貌似囧哥

http://twwy.net/

这是外国的。

http://tech.paulcz.net/

这个有点像公共的。

http://www.pjhome.net/

    好了,这次就推荐这些,下次推荐一些论坛,开发工具,还有心得。

    写完了,终于可以唠叨几句了。东哥的任务啊,哇靠!!!一个月内入门并且写一个日程管理安卓程序&&上课啊,你以为写代码都跟写作文一样可以瞎扯啊!!!

    唉,老实看书去。android+mysql+js。外加数据结构和c++。。。

    时间好快啊,做了周计划,然后睡觉去吧。

    还有,小草的个人网站,可能最近还没做完就是了,又要延期了。
    http://xiaocao.sinaapp.com/
    小草的主博客:http://xiaocaoblog.diandian.com/

2013-11-19 02:15

夏日小草

查看原文

litanda 收藏了文章 · 2019-10-30

谈谈前端异常捕获与上报

关于

前言

Hello,大家好,又与大家见面了,这次给大家分享下前端异常监控中需要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。

首先,我们为什么要进行异常捕获和上报呢?

正所谓百密一疏,一个经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失,因此对于直面用户的前端而言,异常捕获与上报是至关重要的。

虽然目前市面上已经有一些非常完善的前端监控系统存在,如sentrybugsnag等,但是知己知彼,才能百战不殆,唯有了解原理,摸清逻辑,使用起来才能得心应手。

异常捕获方法

1. try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
    var a = 1;
    var b = a + c;
} catch (e) {
    // 捕获处理
    console.log(e); // ReferenceError: c is not defined
}

使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。

2. window.onerror

相比try catch来说window.onerror提供了全局监听异常的功能:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
    console.log('errorMessage: ' + errorMessage); // 异常信息
    console.log('scriptURI: ' + scriptURI); // 异常文件路径
    console.log('lineNo: ' + lineNo); // 异常行号
    console.log('columnNo: ' + columnNo); // 异常列号
    console.log('error: ' + error); // 异常堆栈信息
};

console.log(a);

如图:

window.onerror即提供了我们错误的信息,还提供了错误行列号,可以精准的进行定位,如此似乎正是我们想要的,但是接下来便是填坑过程。

异常捕获问题

1. Script error.

我们合乎情理地在本地页面进行尝试捕获异常,如:

<!-- http://localhost:3031/ -->
<script>
window.onerror = function() {
    console.log(arguments);
};
</script>
<script data-original="http://cdn.xxx.com/index.js"></script>

这里我们把静态资源放到异域上进行优化加载,但是捕获的异常信息却是:

经过分析发现,跨域之后window.onerror是无法捕获异常信息的,所以统一返回Script error.,解决方案便是script属性配置 crossorigin="anonymous" 并且服务器添加Access-Control-Allow-Origin。

<script data-original="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

一般的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是所有域都可以访问。

2. sourceMap

解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。如图,我们用webpack将代码打包压缩成bundle.js:

// webpack.config.js
var path = require('path');

// webpack 4.1.1
module.exports = {
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

最后我们页面引入的脚本文件是这样的:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;

所以我们看到的异常信息是这样的:

lineNo可能是一个非常小的数字,一般是1,而columnNo会是一个很大的数字,这里是730,因为所有代码都压缩到了一行。

那么该如何解决呢?聪明的童鞋可能已经猜到启用source-map了,没错,我们利用webpack打包压缩后生成一份对应脚本的map文件就能进行追踪了,在webpack中开启source-map功能:

module.exports = {
    ...
    devtool: '#source-map',
    ...
}

打包压缩的文件末尾会带上这样的注释:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;
//# sourceMappingURL=bundle.js.map

意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:

version: 3, // Source map的版本
sources: ["webpack:///webpack/bootstrap", ...], // 转换前的文件
names: ["installedModules", "__webpack_require__", ...], // 转换前的所有变量名和属性名
mappings: "aACA,IAAAA,KAGA,SAAAC...", // 记录位置信息的字符串
file: "bundle.js", // 转换后的文件名
sourcesContent: ["// The module cache var installedModules = {};..."], // 源代码
sourceRoot: "" // 转换前的文件所在的目录

如果你想详细了解关于sourceMap的知识,可以前往:JavaScript Source Map 详解

如此,既然我们拿到了对应脚本的map文件,那么我们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。

3. MVVM框架

现在越来越多的项目开始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror来捕获异常,那么很可能会竹篮打水一场空,或许根本捕获不到,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常

Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 异常信息
        name, // 异常名称
        script,  // 异常脚本url
        line,  // 异常行号
        column,  // 异常列号
        stack  // 异常堆栈信息
    } = err;
    
    // vm为抛出异常的 Vue 实例
    // info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}

目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。

同样的在react也提供了异常处理的方式,在 React 16.x 版本中引入了 Error Boundary:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
        
        // 将异常信息上报给服务器
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return '出错了';
        }
    
        return this.props.children;
    }
}

然后我们就可以这样使用该组件:

<ErrorBoundary>
    <MyWidget />
</ErrorBoundary>

详见官方文档:Error Handling in React 16

异常上报

以上介绍了前端异常捕获的相关知识点,那么接下来我们既然成功捕获了异常,那么该如何上报呢?

在脚本代码没有被压缩的情况下可以直接捕获后上传对应的异常信息,这里就不做介绍了,下面主要讲解常见的处理压缩文件上报的方法。

1. 提交异常

当捕获到异常时,我们可以将异常信息传递给接口,以window.onerror为例:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // 构建错误对象
    var errorObj = {
        errorMessage: errorMessage || null,
        scriptURI: scriptURI || null,
        lineNo: lineNo || null,
        columnNo: columnNo || null,
        stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
        var xhr = new XMLHttpRequest();
    
        xhr.open('post', '/middleware/errorMsg', true); // 上报给node中间层处理
        xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头
        xhr.send(JSON.stringify(errorObj)); // 发送参数
    }
}

2. sourceMap解析

其实source-map格式的文件是一种数据类型,既然是数据类型那么肯定有解析它的办法,目前市面上也有专门解析它的相应工具包,在浏览器环境或者node环境下比较流行的是一款叫做'source-map'的插件。

通过require该插件,前端浏览器可以对map文件进行解析,但因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器,中间层代码如下:

const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);

// 定义post接口
router.post('/errorMsg/', function(req, res) {
    let error = req.body; // 获取前端传过来的报错对象
    let url = error.scriptURI; // 压缩文件路径

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径

        // 解析sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象
        
        smc.then(function(result) {
        
            // 解析原始报错数据
            let ret = result.originalPositionFor({
                line: error.lineNo, // 压缩后的行号
                column: error.columnNo // 压缩后的列号
            });
            
            let url = ''; // 上报地址
        
            // 将异常上报至后台
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // 报错信息
                    source: ret.source, // 报错文件路径
                    line: ret.line, // 报错文件行号
                    column: ret.column, // 报错文件列号
                    stack: error.stack // 报错堆栈
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) {
                res.json(json);         
            });
        })
    }
});

module.exports = router;

这里我们通过前端传过来的异常文件路径获取服务器端map文件地址,然后将压缩后的行列号传递给sourceMap返回的promise对象进行解析,通过originalPositionFor方法我们能获取到原始的报错行列号和文件地址,最后通过ajax将需要的异常信息统一传递给后台存储,完成异常上报。下图可以看到控制台打印出了经过解析后的真是报错位置和文件:

附:source-map API

3. 注意点

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点需要注意。

最终大致的流程图如下:

结语

前端异常捕获与上报是前端异常监控的前提,了解并做好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。本文详细实例代码地址:https://github.com/luozhihao/error-catch-report

查看原文

litanda 收藏了文章 · 2019-10-30

vue-cli脚手架中webpack配置基础文件详解

一、前言

vue-cli是构建vue单页应用的脚手架,输入一串指定的命令行从而自动生成vue.js+webpack的项目模板。这其中webpack发挥了很大的作用,它使得我们的代码模块化,引入一些插件帮我们完善功能可以将文件打包压缩,图片转base64等。后期对项目的配置使得我们对于脚手架自动生成的代码的理解更为重要,接下来我将基于webpack3.6.0版本结合文档将文件各个击破,纯干料。
重点章节点击查看:package.jsonconfig/index.jswebpack.base.conf.jswebpack.dev.conf.jswebpack.prod.conf.js

二、主体结构

├─build 
├─config 
├─dist
├─node_modules
├─src
│ ├─assets
│ ├─components
│ ├─router
│ ├─App.vue
│ ├─main.js
├─static
├─.babelrc
├─.editorconfig
├─.gitignore
├─.postcssrc.js
├─index.html
├─package-lock.json
├─package.json
└─README.md

1、 package.json

项目作为一个大家庭,每个文件都各司其职。package.json来制定名单,需要哪些npm包来参与到项目中来,npm install命令根据这个配置文件增减来管理本地的安装包。

{
//从name到private都是package的配置信息,也就是我们在脚手架搭建中输入的项目描述
  "name": "shop",//项目名称:不能以.(点)或者_(下划线)开头,不能包含大写字母,具有明确的的含义与现有项目名字不重复
  "version": "1.0.0",//项目版本号:遵循“大版本.次要版本.小版本”
  "description": "A Vue.js project",//项目描述
  "author": "qietuniu",//作者名字
  "private": true,//是否私有
  //scripts中的子项即是我们在控制台运行的脚本的缩写
  "scripts": {
   //①webpack-dev-server:启动了http服务器,实现实时编译;
   //inline模式会在webpack.config.js入口配置中新增webpack-dev-server/client?http://localhost:8080/的入口,使得我们访问路径为localhost:8080/index.html(相应的还有另外一种模式Iframe);
   //progress:显示打包的进度
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",  
    "start": "npm run dev",//与npm run dev相同,直接运行开发环境
    "build": "node build/build.js"//使用node运行build文件
  },
  //②dependencies(项目依赖库):在安装时使用--save则写入到dependencies
  "dependencies": {
    "vue": "^2.5.2",//vue.js
    "vue-router": "^3.0.1"//vue的路由插件
  },
  //和devDependencies(开发依赖库):在安装时使用--save-dev将写入到devDependencies
  "devDependencies": {
    "autoprefixer": "^7.1.2",//autoprefixer作为postcss插件用来解析CSS补充前缀,例如 display: flex会补充为display:-webkit-box;display: -webkit-flex;display: -ms-flexbox;display: flex。
    //babel:以下几个babel开头的都是针对es6解析的插件。用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本
    "babel-core": "^6.22.1",//babel的核心,把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。
    "babel-helper-vue-jsx-merge-props": "^2.0.3",//预制babel-template函数,提供给vue,jsx等使用
    "babel-loader": "^7.1.1",//使项目运行使用Babel和webpack来传输js文件,使用babel-core提供的api进行转译
    "babel-plugin-syntax-jsx": "^6.18.0",//支持jsx
    "babel-plugin-transform-runtime": "^6.22.0",//避免编译输出中的重复,直接编译到build环境中
    "babel-plugin-transform-vue-jsx": "^3.5.0",//babel转译过程中使用到的插件,避免重复
    "babel-preset-env": "^1.3.2",//转为es5,transform阶段使用到的插件之一
    "babel-preset-stage-2": "^6.22.0",//ECMAScript第二阶段的规范
    "chalk": "^2.0.1",//用来在命令行输出不同颜色文字
    "copy-webpack-plugin": "^4.0.1",//拷贝资源和文件
    "css-loader": "^0.28.0",//webpack先用css-loader加载器去解析后缀为css的文件,再使用style-loader生成一个内容为最终解析完的css代码的style标签,放到head标签里
    "extract-text-webpack-plugin": "^3.0.0",//将一个以上的包里面的文本提取到单独文件中
    "file-loader": "^1.1.4",//③打包压缩文件,与url-loader用法类似
    "friendly-errors-webpack-plugin": "^1.6.1",//识别某些类别的WebPACK错误和清理,聚合和优先排序,以提供更好的开发经验
    "html-webpack-plugin": "^2.30.1",//简化了HTML文件的创建,引入了外部资源,创建html的入口文件,可通过此项进行多页面的配置
    "node-notifier": "^5.1.2",//支持使用node发送跨平台的本地通知
    "optimize-css-assets-webpack-plugin": "^3.2.0",//压缩提取出的css,并解决ExtractTextPlugin分离出的js重复问题(多个文件引入同一css文件)
    "ora": "^1.2.0",//加载(loading)的插件
    "portfinder": "^1.0.13",//查看进程端口
    "postcss-import": "^11.0.0",//可以消耗本地文件、节点模块或web_modules
    "postcss-loader": "^2.0.8",//用来兼容css的插件
    "postcss-url": "^7.2.1",//URL上重新定位、内联或复制
    "rimraf": "^2.6.0",//节点的UNIX命令RM—RF,强制删除文件或者目录的命令
    "semver": "^5.3.0",//用来对特定的版本号做判断的
    "shelljs": "^0.7.6",//使用它来消除shell脚本在UNIX上的依赖性,同时仍然保留其熟悉和强大的命令,即可执行Unix系统命令
    "uglifyjs-webpack-plugin": "^1.1.1",//压缩js文件
    "url-loader": "^0.5.8",//压缩文件,可将图片转化为base64
    "vue-loader": "^13.3.0",//VUE单文件组件的WebPACK加载器
    "vue-style-loader": "^3.0.1",//类似于样式加载程序,您可以在CSS加载器之后将其链接,以将CSS动态地注入到文档中作为样式标签
    "vue-template-compiler": "^2.5.2",//这个包可以用来预编译VUE模板到渲染函数,以避免运行时编译开销和CSP限制
    "webpack": "^3.6.0",//打包工具
    "webpack-bundle-analyzer": "^2.9.0",//可视化webpack输出文件的大小
    "webpack-dev-server": "^2.9.1",//提供一个提供实时重载的开发服务器
    "webpack-merge": "^4.1.0"//它将数组和合并对象创建一个新对象。如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回的值封装在函数中
  },
  //engines是引擎,指定node和npm版本
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  //限制了浏览器或者客户端需要什么版本才可运行
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}

注释:
①、点这里→webpack运行时的配置文档传送门

②、devDependencies和dependencies的区别: devDependencies里面的插件只用于开发环境,不用于生产环境,即辅助作用,打包的时候需要,打包完成就不需要了。而dependencies是需要发布到生产环境的,自始至终都在。比如webpack等只是在开发中使用的包就写入到devDependencies,而像vue这种项目全程依赖的包要写入到dependencies
点这里→更多安装包文档搜索页传送门

③、file-loader和url-loader的区别:以图片为例,file-loader可对图片进行压缩,但是还是通过文件路径进行引入,当http请求增多时会降低页面性能,而url-loader通过设定limit参数,小于limit字节的图片会被转成base64的文件,大于limit字节的将进行图片压缩的操作。总而言之,url-loader是file-loader的上层封装。
点这里→file-loader 和 url-loader详解
点这里→file-loader文档传送门
点这里→url-loader文档传送门

2、.postcssrc.js

.postcssrc.js文件其实是postcss-loader包的一个配置,在webpack的旧版本可以直接在webpack.config.js中配置,现版本中postcss的文档示例独立出.postcssrc.js,里面写进去需要使用到的插件

module.exports = {
  "plugins": {
    "postcss-import": {},//①
    "postcss-url": {},//②
    "autoprefixer": {}//③
  }
}

注释:
①、点这里→postcss-import文档传送门
②、点这里→postcss-url文档传送门
③、点这里→autoprefixer文档传送门

3、 .babelrc

该文件是es6解析的一个配置

{
//制定转码的规则
  "presets": [
  //env是使用babel-preset-env插件将js进行转码成es5,并且设置不转码的AMD,COMMONJS的模块文件,制定浏览器的兼容
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  
  "plugins": ["transform-vue-jsx", "transform-runtime"]//①
}

注释:
①、点这里→transform-vue-jsx文档传送门
点这里→transform-runtime文档传送门

4、src内文件

我们开发的代码都存放在src目录下,根据需要我们通常会再建一些文件夹。比如pages的文件夹,用来存放页面让components文件夹专门做好组件的工作;api文件夹,来封装请求的参数和方法;store文件夹,使用vuex来作为vue的状态管理工具,我也常叫它作前端的数据库等。


①、assets文件:脚手架自动会放入一个图片在里面作为初始页面的logo。平常我们使用的时候会在里面建立js,css,img,fonts等文件夹,作为静态资源调用

②、components文件夹:用来存放组件,合理地使用组件可以高效地实现复用等功能,从而更好地开发项目。一般情况下比如创建头部组件的时候,我们会新建一个header的文件夹,然后再新建一个header.vue的文件

③、router文件夹:该文件夹下有一个叫index.js文件,用于实现页面的路由跳转,具体使用请点击→vue-router传送门

④、App.vue:作为我们的主组件,可通过使用<router-view/>开放入口让其他的页面组件得以显示。

⑤、main.js:作为我们的入口文件,主要作用是初始化vue实例并使用需要的插件,小型项目省略router时可放在该处

注释:具体vue的用法可查看vue官方中文文档传送门

5、其他文件

①、.editorconfig:编辑器的配置文件

②、.gitignore:忽略git提交的一个文件,配置之后提交时将不会加载忽略的文件

③、index.html:页面入口,经过编译之后的代码将插入到这来。

④、package.lock.json:锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致

⑤、README.md:可此填写项目介绍

⑥、node_modules:根据package.json安装时候生成的的依赖(安装包)

三、config文件夹

├─config 
│ ├─dev.env.js 
│ ├─index.js 
│ ├─prod.env.js 

1、config/dev.env.js

config内的文件其实是服务于build的,大部分是定义一个变量export出去。

'use strict'//采用严格模式
const merge = require('webpack-merge')//①
const prodEnv = require('./prod.env')
//webpack-merge提供了一个合并函数,它将数组和合并对象创建一个新对象。
//如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回的值封装在函数中.这边将dev和prod进行合并
module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})

注释:①、点这里→webpack-merge文档传送门

2、config/prod.env.js

当开发是调取dev.env.js的开发环境配置,发布时调用prod.env.js的生产环境配置

'use strict'
module.exports = {
  NODE_ENV: '"production"'
}

3、config/index.js

'use strict'
const path = require('path')

module.exports = {
  dev: {
    // 开发环境下面的配置
    assetsSubDirectory: 'static',//子目录,一般存放css,js,image等文件
    assetsPublicPath: '/',//根目录
    proxyTable: {},//可利用该属性解决跨域的问题
    host: 'localhost', // 地址
    port: 8080, //端口号设置,端口号占用出现问题可在此处修改
    autoOpenBrowser: false,//是否在编译(输入命令行npm run dev)后打开http://localhost:8080/页面,以前配置为true,近些版本改为false,个人偏向习惯自动打开页面
    errorOverlay: true,//浏览器错误提示
    notifyOnErrors: true,//跨平台错误提示
    poll: false, //使用文件系统(file system)获取文件改动的通知devServer.watchOptions
    devtool: 'cheap-module-eval-source-map',//增加调试,该属性为原始源代码(仅限行)不可在生产环境中使用
    cacheBusting: true,//使缓存失效
    cssSourceMap: true//代码压缩后进行调bug定位将非常困难,于是引入sourcemap记录压缩前后的位置信息记录,当产生错误时直接定位到未压缩前的位置,将大大的方便我们调试
  },

  build: {
  // 生产环境下面的配置
    index: path.resolve(__dirname, '../dist/index.html'),//index编译后生成的位置和名字,根据需要改变后缀,比如index.php
    assetsRoot: path.resolve(__dirname, '../dist'),//编译后存放生成环境代码的位置
    assetsSubDirectory: 'static',//js,css,images存放文件夹名
    assetsPublicPath: '/',//发布的根目录,通常本地打包dist后打开文件会报错,此处修改为./。如果是上线的文件,可根据文件存放位置进行更改路径
    productionSourceMap: true,
    devtool: '#source-map',//①
    //unit的gzip命令用来压缩文件,gzip模式下需要压缩的文件的扩展名有js和css
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    bundleAnalyzerReport: process.env.npm_config_report
  }
}

注释:①点击→devtool文档传送门

四、build文件夹

├─build 
│ ├─build.js 
│ ├─check-versions.js 
│ ├─utils.js 
│ ├─vue-loader.conf.js 
│ ├─webpack.base.conf.js 
│ ├─webpack.dev.conf.js 
│ ├─webpack.prod.conf.js 

1、build/build.js

该文件作用,即构建生产版本。package.json中的scripts的build就是node build/build.js,输入命令行npm run build对该文件进行编译生成生产环境的代码。

'use strict'
require('./check-versions')()//check-versions:调用检查版本的文件。加()代表直接调用该函数
process.env.NODE_ENV = 'production'//设置当前是生产环境
//下面定义常量引入插件
const ora = require('ora')//①加载动画
const rm = require('rimraf')//②删除文件
const path = require('path')
const chalk = require('chalk')//③对文案输出的一个彩色设置
const webpack = require('webpack')
const config = require('../config')//默认读取下面的index.js文件
const webpackConfig = require('./webpack.prod.conf')
//调用start的方法实现加载动画,优化用户体验
const spinner = ora('building for production...')
spinner.start()
//先删除dist文件再生成新文件,因为有时候会使用hash来命名,删除整个文件可避免冗余
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      process.exit(1)
    }

    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'
    ))
  })
})

注释:
①、点这里→ora文档传送门
②、点这里→chalk文档传送门
③、点这里→rimraf文档传送门

2、build/check-version.js

该文件用于检测node和npm的版本,实现版本依赖

'use strict'
const chalk = require('chalk')
const semver = require('semver')//①对版本进行检查
const packageConfig = require('../package.json')
const shell = require('shelljs')

function exec (cmd) {
//返回通过child_process模块的新建子进程,执行 Unix 系统命令后转成没有空格的字符串
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),//使用semver格式化版本
    versionRequirement: packageConfig.engines.node//获取package.json中设置的node版本
  }
]

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),// 自动调用npm --version命令,并且把参数返回给exec函数,从而获取纯净的版本号
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []
  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
    //上面这个判断就是如果版本号不符合package.json文件中指定的版本号,就执行下面错误提示的代码
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  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.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}

注释:
①、点这里→chalk文档传送门
点这里→semver文档传送门

3、build/utils.js

utils是工具的意思,是一个用来处理css的文件。

'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
//导出文件的位置,根据环境判断开发环境和生产环境,为config文件中index.js文件中定义的build.assetsSubDirectory或dev.assetsSubDirectory
exports.assetsPath = function (_path) {
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
//Node.js path 模块提供了一些用于处理文件路径的小工具①
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}
//使用了css-loader和postcssLoader,通过options.usePostCSS属性来判断是否使用postcssLoader中压缩等方法
  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        //Object.assign是es6语法的浅复制,后两者合并后复制完成赋值
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }
    
    if (options.extract) {
    //ExtractTextPlugin可提取出文本,代表首先使用上面处理的loaders,当未能正确引入时使用vue-style-loader
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
    //返回vue-style-loader连接loaders的最终值
      return ['vue-style-loader'].concat(loaders)
    }
  }
  return {
    css: generateLoaders(),//需要css-loader 和 vue-style-loader
    postcss: generateLoaders(),//需要css-loader和postcssLoader  和 vue-style-loader
    less: generateLoaders('less'),//需要less-loader 和 vue-style-loader
    sass: generateLoaders('sass', { indentedSyntax: true }),//需要sass-loader 和 vue-style-loader
    scss: generateLoaders('sass'),//需要sass-loader 和 vue-style-loader
    stylus: generateLoaders('stylus'),//需要stylus-loader 和 vue-style-loader
    styl: generateLoaders('stylus')//需要stylus-loader 和 vue-style-loader
  }
}
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)
    //将各种css,less,sass等综合在一起得出结果输出output
  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

exports.createNotifierCallback = () => {
//发送跨平台通知系统
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return
//当报错时输出错误信息的标题,错误信息详情,副标题以及图标
    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}

注释:
①、path.posix:提供对路径方法的POSIX(可移植性操作系统接口)特定实现的访问,即可跨平台,区别于win32。
path.join:用于连接路径,会正确使用当前系统的路径分隔符,Unix系统是"/",Windows系统是""
点击→path用法传送门

4、vue-loader.conf.js

该文件的主要作用就是处理.vue文件,解析这个文件中的每个语言块(template、script、style),转换成js可用的js模块。

'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap
//处理项目中的css文件,生产环境和测试环境默认是打开sourceMap,而extract中的提取样式到单独文件只有在生产环境中才需要
module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
   // 在模版编译过程中,编译器可以将某些属性,如 src 路径,转换为require调用,以便目标资源可以由 webpack 处理.
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}

5、webpack.base.conf.js

webpack.base.conf.js是开发和生产共同使用提出来的基础配置文件,主要实现配制入口,配置输出环境,配置模块resolve和插件等

'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
//拼接出绝对路径
  return path.join(__dirname, '..', dir)
}
module.exports = {
//path.join将路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃
//path.join('/a', '/b') // 'a/b',path.resolve('/a', '/b') // '/b'
  context: path.resolve(__dirname, '../'),
  //配置入口,默认为单页面所以只有app一个入口
  entry: {
    app: './src/main.js'
  },
  //配置出口,默认是/dist作为目标文件夹的路径
  output: {
    path: config.build.assetsRoot,//路径
    filename: '[name].js',//文件名
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath//公共存放路径
  },
  resolve: {
  //自动的扩展后缀,比如一个js文件,则引用时书写可不要写.js
    extensions: ['.js', '.vue', '.json'],
    //创建路径的别名,比如增加'components': resolve('src/components')等
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
  //使用插件配置相应文件的处理方法
  module: {
    rules: [
    //使用vue-loader将vue文件转化成js的模块①
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      //js文件需要通过babel-loader进行编译成es5文件以及压缩等操作②
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      //图片、音像、字体都使用url-loader进行处理,超过10000会编译成base64③
      {
        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]')
        }
      }
    ]
  },
  //以下选项是Node.js全局变量或模块,这里主要是防止webpack注入一些Node.js的东西到vue中 
  node: 
    setImmediate: false,
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}

注释:
①、点击→vue-loader文档传送门
②、点击→babel-loader文档传送门

6、webpack.dev.conf.js

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
//通过webpack-merge实现webpack.dev.conf.js对webpack.base.config.js的继承
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
//美化webpack的错误信息和日志的插件①
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')// 查看空闲端口位置,默认情况下搜索8000这个端口②
const HOST = process.env.HOST//③processs为node的一个全局对象获取当前程序的环境变量,即host
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
  //规则是工具utils中处理出来的styleLoaders,生成了css,less,postcss等规则
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },

  devtool: config.dev.devtool,  //增强调试,上文有提及
  //此处的配置都是在config的index.js中设定好了
  devServer: {//④
    clientLogLevel: 'warning',//控制台显示的选项有none, error, warning 或者 info
    //当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,//热加载
    contentBase: false,
    compress: true,//压缩
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,//调试时自动打开浏览器
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,// warning 和 error 都要显示
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,//接口代理
    quiet: true, //控制台是否禁止打印警告和错误,若用FriendlyErrorsPlugin 此处为 true
    watchOptions: {
      poll: config.dev.poll,//// 文件系统检测改动
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),//⑤模块热替换插件,修改模块时不需要刷新页面
    new webpack.NamedModulesPlugin(), // 显示文件的正确名字
    new webpack.NoEmitOnErrorsPlugin(),//当webpack编译错误的时候,来中端打包进程,防止错误代码打包到文件中
    // https://github.com/ampedandwired/html-webpack-plugin
    // 该插件可自动生成一个 html5 文件或使用模板文件将编译好的代码注入进去⑥
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new CopyWebpackPlugin([//复制插件
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']//忽略.*的文件
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  //查找端口号
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
    //端口被占用时就重新设置evn和devServer的端口
      process.env.PORT = port
      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)
    }
  })
})

注释:
①、点击→friendly-errors-webpack-plugin文档传送门
②、点击→process文档传送门
③、点击→babel-loader文档传送门
④、点击→devtool文档传送门
⑤、点击→webpack的HotModuleReplacementPlugin文档传送门
⑥、点击→html-webpack-plugin文档传送门

7、webpack.prod.conf.js

'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = require('../config/prod.env')

const webpackConfig = merge(baseWebpackConfig, {
  module: {
  //调用utils.styleLoaders的方法
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,//开启调试的模式。默认为true
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {//压缩
          warnings: false//警告:true保留警告,false不保留
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    new ExtractTextPlugin({//抽取文本。比如打包之后的index页面有style插入,就是这个插件抽取出来的,减少请求
      filename: utils.assetsPath('css/[name].[contenthash].css'),  
      allChunks: true,
    }),
    
    new OptimizeCSSPlugin({//优化css的插件
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
   
    new HtmlWebpackPlugin({//html打包
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {//压缩
        removeComments: true,//删除注释
        collapseWhitespace: true,//删除空格
        removeAttributeQuotes: true//删除属性的引号   
      },
     
      chunksSortMode: 'dependency'//模块排序,按照我们需要的顺序排序
    }),
   
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.optimize.CommonsChunkPlugin({//抽取公共的模块
      name: 'vendor',
      minChunks (module) {   
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
    new CopyWebpackPlugin([//复制,比如打包完之后需要把打包的文件复制到dist里面
      {
        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.prod.conf.js详细内容

五、结语

第一篇博文总在想要写点什么,就根据自己的经验加查了下文档写了这么一篇工具类的文章,由于有些插件有些用法会重复,所以按照文章先后,写过用法或者给过链接的插件,在后面的文章就省略了,有时间的建议从头开始,如果单独看某章节的话遇到不懂的语法或插件可全文查找,也可以点击更多安装包传送门进行查找阅读。本文将vue本身自带的英文注释删除了,但英文注释非常有用可以仔细阅读,希望对大家学习vue和webpack都有所帮助。

尊重原创,如需转载请注明出处!

查看原文

litanda 收藏了文章 · 2019-10-30

webpack原理

webpack原理

查看所有文档页面:前端开发文档,获取更多信息。
原文链接:webpack原理,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。

工作原理概括

基本概念

在了解 Webpack 原理前,需要掌握以下几个核心概念,以方便后面的理解:

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

Webpack 的构建流程可以分为以下三大阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  2. 编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  3. 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

如果只执行一次构建,以上阶段将会按照顺序各执行一次。但在开启监听模式下,流程将变为如下:

在每个大阶段中又会发生很多事件,Webpack 会把这些事件广播出来供给 Plugin 使用,下面来一一介绍。

初始化阶段

事件名解释
初始化参数从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()
实例化 Compiler用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
entry-option读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
after-plugins调用完所有内置的和配置的插件的 apply 方法。
after-resolvers根据配置初始化完 resolverresolver 负责在文件系统中寻找指定路径的文件。
空格空格
空格空格
空格空格

编译阶段

事件名解释
run启动一次新的编译。
watch-runrun 类似,区别在于它是在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。
compile该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。
compilationWebpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
make一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
after-compile一次 Compilation 执行完成。
invalid当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致 Webpack 退出。
空格空格
空格空格

在编译阶段中,最重要的要数 compilation 事件了,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件,它们分别是:

事件名解释
build-module使用对应的 Loader 去转换一个模块。
normal-module-loader在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
program从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
seal所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。

输出阶段

事件名解释
should-emit所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
after-emit文件输出完毕。
done成功完成一次完成的编译和输出流程。
failed如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。

输出文件分析

虽然在前面的章节中你学会了如何使用 Webpack ,也大致知道其工作原理,可是你想过 Webpack 输出的 bundle.js 是什么样子的吗? 为什么原来一个个的模块文件被合并成了一个单独的文件?为什么 bundle.js 能直接运行在浏览器中? 本节将解释清楚以上问题。

先来看看由 安装与使用 中最简单的项目构建出的 bundle.js 文件内容,代码如下:

<p data-height="565" data-theme-id="0" data-slug-hash="NMQzxz" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

以上看上去复杂的代码其实是一个立即执行函数,可以简写为如下:

(function(modules) {

  // 模拟 require 语句
  function __webpack_require__() {
  }

  // 执行存放所有模块数组中的第0个模块
  __webpack_require__(0);

})([/*存放所有模块的数组*/])

bundle.js 能直接运行在浏览器中的原因在于输出的文件中通过 __webpack_require__ 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js 中的 require 语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

如果仔细分析 __webpack_require__ 函数的实现,你还有发现 Webpack 做了缓存优化: 执行加载过的模块不会再执行第二次,执行结果会缓存在内存中,当某个模块第二次被访问时会直接去内存中读取被缓存的返回值。

分割代码时的输出

例如把源码中的 main.js 修改为如下:

// 异步加载 show.js
import('./show').then((show) => {
  // 执行 show 函数
  show('Webpack');
});

重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和 异步加载文件 0.bundle.js

其中 0.bundle.js 内容如下:

// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
  // 在其它文件中存放着的模块的 ID
  [0],
  // 本文件所包含的模块
  [
    // show.js 所对应的模块
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js 内容如下:

<p data-height="565" data-theme-id="0" data-slug-hash="yjmRyG" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="bundle.js" class="codepen">See the Pen bundle.js by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

这里的 bundle.js 和上面所讲的 bundle.js 非常相似,区别在于:

  • 多了一个 __webpack_require__.e 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件;
  • 多了一个 webpackJsonp 函数用于从异步加载的文件中安装模块。

在使用了 CommonsChunkPlugin 去提取公共代码时输出的文件和使用了异步加载时输出的文件是一样的,都会有 __webpack_require__.ewebpackJsonp。 原因在于提取公共代码和异步加载本质上都是代码分割。

编写 Loader

Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。

以处理 SCSS 文件为例:

  • SCSS 源代码会先交给 sass-loader 把 SCSS 转换成 CSS;
  • sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;
  • css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;

可以看出以上的处理过程需要有顺序的链式执行,先 sass-loadercss-loaderstyle-loader。 以上处理的 Webpack 相关配置如下:

<p data-height="365" data-theme-id="0" data-slug-hash="YLmbeQ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="编写 Loader" class="codepen">See the Pen 编写 Loader by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

Loader 的职责

由上面的例子可以看出:一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的顺序执行, 第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果会传给下一个接着处理,最后的 Loader 将处理后的最终结果返回给 Webpack。

所以,在你开发一个 Loader 时,请保持其职责的单一性,你只需关心输入和输出。

Loader 基础

由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

一个最简单的 Loader 的源码如下:

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};

Loader 进阶

以上只是个最简单的 Loader,Webpack 还提供一些 API 供 Loader 调用,下面来一一介绍。

获得 Loader 的 options

在最上面处理 SCSS 文件的 Webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader。 如何在自己编写的 Loader 中获取到用户传入的 options 呢?需要这样做:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

返回其它结果

上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西。

例如以用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一起随着 ES5 代码返回给 Webpack,可以这样写:

module.exports = function(source) {
  // 通过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};

其中的 this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback 的详细使用方法如下:

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);
Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。 如果你编写的 Loader 会生成 Source Map,请考虑到这点。

同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

在转换步骤是异步时,你可以这样:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

处理二进制数据

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也可以是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。

如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

其它 Loader API

除了以上提到的在 Loader 中能调用的 Webpack API 外,还存在以下常用 API:

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
  • this.resourcePath:当前处理文件的路径,例如 /src/main.js
  • this.resourceQuery:当前处理文件的 querystring
  • this.target:等于 Webpack 配置中的 Target。
  • this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。
  • this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)
  • this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)
  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加载本地 Loader

在开发 Loader 的过程中,为了测试编写的 Loader 是否能正常工作,需要把它配置到 Webpack 中后,才可能会调用该 Loader。 在前面的章节中,使用的 Loader 都是通过 Npm 安装的,要使用 Loader 时会直接使用 Loader 的名称,代码如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};

如果还采取以上的方法去使用本地开发的 Loader 将会很麻烦,因为你需要确保编写的 Loader 的源码是在 node_modules 目录下。 为此你需要先把编写的 Loader 发布到 Npm 仓库后再安装到本地项目使用。

解决以上问题的便捷方法有两种,分别如下:

Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

  • 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  • 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

ResolveLoader

ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:


module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

实战

上面讲了许多理论,接下来从实际出发,来编写一个解决实际问题的 Loader。

该 Loader 名叫 comment-require-loader,作用是把 JavaScript 代码中的注释语法:

// @require '../style/index.css'

转换成:

require('../style/index.css');

该 Loader 的使用场景是去正确加载针对 Fis3 编写的 JavaScript,这些 JavaScript 中存在通过注释的方式加载依赖的 CSS 文件。

该 Loader 的使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};

该 Loader 的实现非常简单,完整代码如下:

function replace(source) {
    // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');  
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};

编写 Plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个最基础的 Plugin 的代码是这样的:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

在使用这个 Plugin 时,相关配置代码如下:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

通过以上最简单的 Plugin 相信你大概明白了 Plugin 的工作原理,但实际开发中还有很多细节需要注意,下面来详细介绍。

CompilerCompilation

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 optionsloadersplugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。

Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

同理,compilation.applycompilation.plugin 使用方法和上面一致。

在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。

在开发插件时,还需要注意以下两点:

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
 compiler.plugin('emit',function(compilation, callback) {
    // 支持处理逻辑

    // 处理完毕后执行 callback 以通知 Webpack 
    // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
    callback();
  });

常用 API

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。

读取输出资源、代码块、模块及其依赖

有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。

emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:

<p data-height="585" data-theme-id="0" data-slug-hash="RJwjPj" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="emit" class="codepen">See the Pen emit by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:

<p data-height="255" data-theme-id="0" data-slug-hash="jKOabJ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Compilation" class="codepen">See the Pen Compilation by whjin (@whjin) on CodePen.</p>
<script async data-original="https://static.codepen.io/ass...;></script>

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

读取 compilation.assets 的代码如下:


compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});

判断 Webpack 使用了哪些插件

在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 Webpack 当前的插件配置情况。 以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

实战

下面我们举一个实际的例子,带你一步步去实现一个插件。

该插件的名称取名叫 EndWebpackPlugin,作用是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。 同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法如下:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败,err 是导致错误的原因
      console.error(err);        
    })
  ]
}

要实现该插件,需要借助两个事件:

  • done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;

实现该插件非常简单,完整代码如下:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // 在 done 事件中回调 doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // 在 failed 事件中回调 failCallback
        this.failCallback(err);
    });
  }
}
// 导出插件 
module.exports = EndWebpackPlugin;

从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。 在 工作原理概括 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。

调试 Webpack

在编写 Webpack 的 Plugin 和 Loader 时,可能执行结果会和你预期的不一样,就和你平时写代码遇到了奇怪的 Bug 一样。 对于无法一眼看出问题的 Bug,通常需要调试程序源码才能找出问题所在。

虽然可以通过 console.log 的方式完成调试,但这种方法非常不方便也不优雅,本节将教你如何断点调试 工作原理概括 中的插件代码。 由于 Webpack 运行在 Node.js 之上,调试 Webpack 就相对于调试 Node.js 程序。

在 Webstorm 中调试

Webstorm 集成了 Node.js 的调试工具,因此使用 Webstorm 调试 Webpack 非常简单。

1. 设置断点

在你认为可能出现问题的地方设下断点,点击编辑区代码左侧出现红点表示设置了断点。

2. 配置执行入口

告诉 Webstorm 如何启动 Webpack,由于 Webpack 实际上就是一个 Node.js 应用,因此需要新建一个 Node.js 类型的执行入口。

以上配置中有三点需要注意:

  • Name 设置成了 debug webpack,就像设置了一个别名,方便记忆和区分;
  • Working directory 设置为需要调试的插件所在的项目的根目录;
  • JavaScript file 即 Node.js 的执行入口文件,设置为 Webpack 的执行入口文件 node_modules/webpack/bin/webpack.js

3. 启动调试

经过以上两步,准备工作已经完成,下面启动调试,启动时选中前面设置的 debug webpack

4. 执行到断点

启动后程序就会停在断点所在的位置,在这里你可以方便的查看变量当前的状态,找出问题。

原理总结

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

对 Webpack 的使用者来说,它是一个简单强大的工具; 对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

通过本章的学习,希望你不仅能学会如何编写 Webpack 扩展,也能从中领悟到如何设计好的系统架构。

查看原文

litanda 收藏了文章 · 2019-10-30

一点感悟:《Node.js学习笔记》star数突破1000+

写作背景

笔者前年开始撰写的《Node.js学习笔记》 github star 数突破了1000,算是个里程碑吧。

从第一次提交(2016.11.03)到现在,1年半过去了。突然有些感慨,想要写点东西,谈谈这期间的收获、心路历程,以及如何学习Node.js。

clipboard.png

clipboard.png

心路历程

笔者一直有做技术笔记的习惯,前几年零零散散的也写了不少Node.js的东西,只不过都存在evernote里。写着写着,觉得有必要系统地整理下,于是就有了这个项目。

粗略统计了下,总共提交了约60篇教程,以及将近300个范例脚本

clipboard.png

大部分的commit都集中2016年11、12月份,以及2017年上半年。这段时间其实项目组挺忙的,经常一周6天班,同时在两三个项目间来回切换。

写作的过程挺枯燥的,也有点累人,尤其经常只能抽大半夜或周末的时间来码字,经常写技术文章的同学应该能体会。不管怎么说,一路坚持了下来,感觉还是有不少收获。

1、技术积累。最初存在evernote里的只是零星的笔记,经过整理校对、进一步的思考以及延展性学习,零散的知识点逐渐串联成体系化的知识面。这比单单记住了数百个Node.js的API,以及枯燥的配置项更有用。

2、知识分享。写作的过程中,不少同样正在学习Node.js的同学或通过QQ,或通过私信表达了感谢。对笔者来说,这其实比star数的增加更有意义。

3、技术焦虑有所缓解。众所周知,前端领域变化太快,身处其中的从业者压力非常大,这也是前不久著名的“老子学不动了”的梗突然刷屏的原因。深入学习、思考,掌握学习的方法和规律,能够一定程度上缓解技术焦虑症。

4、意外收获。这期间,收到阿里云栖社区(专家博主)、腾讯云+社区的入驻邀请,多家知名出版社的撰稿邀请,在线教育平台(如慕课)的开课邀请等。

如何学习Node.js

2年前在SegmentFault社区上有人问过类似的问题《关于nodejs的学习?》,当时简单地回答了下。

  1. 实践是最好的学习方式,如果能把所学用到实际中去,效率比光学不练要高上很多。
  2. 遇到问题,学会使用google、stackoverflow、官方文档。
  3. 学习node的障碍,大部分时候不是node本身,而是相关领域知识。

实践出真知,这点无需强调。遇到技术问题善用搜索引擎,也算是圈内共识了(初学者需要加强这方面意识)。

其实最难的是第3点,分辨你所遇到的问题。

举个例子,比如现在想学习 https 这个模块,不少初学者会显得一筹莫展,常见的问题有:

  1. 问题一:https、http、net 模块长得好像,API也差不多,它们之间是什么关系?
  2. 问题二:配置项里有一项是证书,这是个干嘛的?照着指引配好证书了,为什么浏览器会报错?
  3. 问题三:server本地跑得好好的,怎么部署到云服务器上就访问不了,明明可以ping通,端口也启动了,为什么提示拒绝访问?

正式回答问题前,先祭出一张网络分层架构图,请读者把它牢记在心。

clipboard.png

互联网基于分层架构实现,包括应用层、传输层、网络层、链路层、物理层。其中,前端开发者比较熟悉的是应用层(HTTP协议),如果想学习Node服务端编程,那么,至少需要对传输层(TCP)、网络层(IP)也有一定的了解。

对于网络的每个层次,Node.js基本都有对应的模块,比如https、http、net(TCP)、tls/crypto等。

前面列举的几个问题,都是对网络知识、服务器知识了解的欠缺导致的,而不是于Node.js的API有多复杂、难以理解。

这里直接回答问题:

  1. 问题一:http为应用层模块,主要按照特定协议编解码数据;net为传输层模块,主要负责传输编码后的应用层数据;https是个综合模块(涵盖了http/tls/crypto等),主要用于确保数据安全性;该用哪个模块应该很清楚了。
  2. 问题二:安全证书是PKI体系的重要一环,主要用于身份校验。本地调试用的证书如果是自己签署的话,浏览器会视为不安全并报错,可以参考 《HTTPS科普扫描帖》。
  3. 问题三:这种情况大概率是请求被防火墙拦截。ping走的是ICMP协议,由操作系统内核处理,能够ping通不代表TCP连接就能够建立成功,可以参考 《ping的使用与实现原理剖析

写在后面

编写《Node.js学习笔记》的过程收获了不少,也有不少感触,这里就不过多碎碎念。对于“如何学习Node.js”这个问题,其实有挺多东西想写,篇幅所限,后面的文章详细展开。

相关链接

Nodejs学习笔记
笔者个人博客

图片描述

查看原文

litanda 收藏了文章 · 2019-10-30

简述vue-router实现原理

router源码解读

阅读请关注下代码注释

打个广告:哪位大佬教我下sf怎么排版啊,不会弄菜单二级导航(扑通.gif)

logo.png

<h2>1. router是什么</h2>

首先,你会从源码里面引入Router,然后再传入参数实例化一个路由对象

// router/index.js
import Router from 'vue-router'
new Router({...})
...

源码基础类:

// 源码index.js
export default class VueRouter {
  ...
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'   // 不选择模式会默认使用hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {         // 非浏览器环境默认nodejs环境
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) { // 根据参数选择三种模式的一种
      case 'history':
        this.history = new HTML5History(this, options.base) // 根据HTML5版History的方法和属性实现的模式
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback) // 利用url中的hash特性实现
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base) // 这种模式原理暂不清楚
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
  ...
  // 一些api方法,你应该很熟悉,$router.push(...)
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1)
  }
  ...
}

我们创建的路由都是VueRouter类的实例化,用来管理我们的【key-components-view】,一个key(代码中的path)对应一个组件,view也就是<router-view>在template里面占个坑,用来根据key展示对应的组件,实例上的func让我们可以控制路由,也就是官网的api
说简单点,路由就是一个‘轮播图’,emmmmmm,说轮播好像也不过分哈,写个循环切换key的func就是‘轮播了’,而key就是轮播的index,手动滑稽。那么,vue-router是如何实现不发送请求就更新视图的呢,让我们来看看vue如何使用路由的
实例化后的路由输出:区分下route和router
clipboard.png

2. router工作原理
如果你要使用到router,你会在实例化Vue的参数options中加入router

// main.js
improt xxx from xxx
import router from xxx
new Vue({
  el: '#app',
  router: router,
  components: { App },
  template: '<App/>'
})

那,Vue是如何使用这个参数呢,vue-router是作为插件加入使用的,通过mixin(混合)来影响每一个Vue实例化,在beforeCreate 钩子的时候就会完成router的初始化,从参数获取router -> 调用init初始化 -> 加入响应式(defineReactive方法在vue源码用的很多,也是底层实现响应式的核心方法)clipboard.png

// 源码install.js
Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router   // 获取options里面的router配置
        this._router.init(this)               // 初始化,这个init是VueRouter类里面的方法,实例化后会继承这个方法,方法代码见下方 
        Vue.util.defineReactive(this, '_route', this._router.history.current) // 这个是把_route加入数据监控,所以你可以watch到_route
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

初始化会做些什么:
-判断主程序状态(已经初始化了的vue实例不会再重新初始化路由,也就是你不能手动去再init一次)
-把实例加入内置数组
-判断history的类型,做一些类似优化的事,比如hash模式的setupListeners方法,就是延迟监听hashChange事件,等到vue完成挂载再监听,太细节不用深入
-listen定义一个callback,listen是定义在最底层History类上的,作用就是定义一个callback,listen会在需要的时候被调用,在路由发生变化的时候会执行这个callback

// 源码index.js
export default class VueRouter {
...
init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)   // 这个apps存储了让所有的Vue实例化(根组件),后面遍历的时候,会把当前标记route挂到所有根组件的,也就是 vm._route 也是 vm._router.history.current

    // main app already initialized.
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {               // 注意这个listen会在后面用到
      this.apps.forEach((app) => {
        app._route = route                  // 根组件全部获取当前route
      })
    })
  }
...
}

关于route的变化过程会在下面具体模式中说明,这里先跳过,接下来先说vue拿到router后,怎么使用router来渲染组件的

3. vue如何使用router的

在安装vue-router插件的时候

export function install (Vue) {
  ...
  Vue.component('RouterView', View)  // <router-link> & <router-view> 你应该很熟悉,本质就是vue组件,看源码之前我的猜测也是组件
  Vue.component('RouterLink', Link)
  ...
}

router-link你不一定会使用,但是router-view你肯定会使用,它就是作为'窗口'的存在,来渲染你需要展示的组件。
那,从这个组件开始说,一个前提条件是:vnode是通过render来创建的,也就是说改变_route的值会执行render函数,Router-View这个组件定义了自己的render,省略了大部分代码,这两行够了,你最终通过<router-view>看到的视图就是这么来的

// vue源码render.js
export function renderMixin (Vue: Class<Component>) {
...
vnode = render.call(vm._renderProxy, vm.$createElement)
...
}
// router源码 view.js
render (_, { props, children, parent, data }) {
...
const h = parent.$createElement
...
return h(component, data, children)
}

第一种:hashHistory模式

流程

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

1. 关于hash
url中#号后面的参数,别名哈希值,关于hash的一些特性

1.改变hash并不会引起页面重载
2.HTTP请求不包括#,所以使用hash不会影响到其他功能
3.改变#会改变浏览器的访问历史
4.window.location.hash可以读取哈希值
5.JavaScript可以通过onhashchange监听到hash值的变化,这就意味着可以知道用户在浏览器手动改变了hash值

clipboard.png

clipboard.png

因为这些特性才有的hashHistory
更多关于hash知识见 URL的井号 - 阮一峰的网络日志

2. hashHistory源码
首先,这三种模式都是通过继承一个基础类History来的

export class HashHistory extends History {
...
}

那,三种模式肯定有相同的属性,相同的方法,肯定不会去创建三次所以从一个基类继承,然后各自的部分属性or方法会有差异,至于History这个类,我是不会去细看的,反正我也看不懂,哈哈哈哈

clipboard.png

router上的实例属性、方法可以在VueRouter、HashHistory/HTML5History/AbstractHistory、History上找到,这里说下HashHistory的几个func的实现、

// router源码hash.js
export class HTML5History extends History {
...
go (n: number) {
    window.history.go(n)
  }
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {  // History类上的func
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

function pushHash (path) {
  if (supportsPushState) { // 是否浏览器环境且环境支持pushstat方法,这个func下面会说
    pushState(getUrl(path)) // 支持的话往window.history添加一条数据
  } else {
    window.location.hash = path // 不支持的话直接修改location的hash
  }
}

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
// 其实replace和push只有两个区别
1.
window.location.hash = path
window.location.replace(getUrl(path))
2.
if (replace) { // replace调这个func会传一个true
  history.replaceState({ key: _key }, '', url)
} else {
  _key = genKey()
  history.pushState({ key: _key }, '', url)
}
...
}

还有一点就是,在初始化hash模式路由的时候,会执行一个func,监听hashchange事件

setupListeners () {
    window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    })
}

第二种:HTML5History模式

HTML5--History 科普

主要是新增的两个api

1.History.pushState()

clipboard.png
[优点写的清清楚楚]
clipboard.png

clipboard.png

HTML5History的push、replace跟hash模式的差不多,就不上代码了
一个标记是否支持HTML5的flag,这么写的,有需要的可以刨回去用

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()

还有一个就是scrollBehavior,用来记录路由跳转的时候滚动条的位置,这个只能在HTML5模式下使用,即支持pushState方法的时候,部分博客说只有在HTML5History下才能使用,这个等我明天验证一下,我个人觉得支持HTML5就可以了

2.History.replaceState()

clipboard.png
说的也很直观,就是不创新新纪录而覆盖一条记录,just do it

结束语

别问第三种情况(我是谁、我在哪、谁打我)

我兜子好沃,早知道不做前端了~

在学习router源码的时候阅读了熵与单子的代码本的文章,看完这篇文章配合源码基本都可以很好掌握vue-router的大概,感谢作者,另外说明下本文由本人学习结束后加上自己的理解一字一字敲出来的,可能有些相似之处,侵删请联系我,写文章的目的是看看自己能否表述清楚,对知识点的掌握情况,讲的不对的地方,请各位大佬指正~

~感谢潘童鞋的指导(^▽^)

当然,我也稀罕你的小❤❤,点个赞再走咯~

以上图片均来自MDN网页截图、vue官网截图、百度首页截图,不存在版权问题 /滑稽

【注】:内容有不当或者错误处请指正~转载请注明出处~谢谢合作!

查看原文

认证与成就

  • 获得 0 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-01
个人主页被 117 人浏览