SegmentFault 前端小将最新的文章
2019-05-09T08:47:46+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Webpack 是怎样运行的?
https://segmentfault.com/a/1190000019117897
2019-05-09T08:47:46+08:00
2019-05-09T08:47:46+08:00
Alan
https://segmentfault.com/u/xc_xiang
62
<p>在平时开发中我们经常会用到<code>Webpack</code>这个时下最流行的前端打包工具。它打包开发代码,输出能在各种浏览器运行的代码,提升了开发至发布过程的效率。</p>
<p>我们知道一份<code>Webpack</code>配置文件主要包含入口(<code>entry</code>)、输出文件(<code>output</code>)、模式、加载器(<code>Loader</code>)、插件(<code>Plugin</code>)等几个部分。但如果只需要组织 JS 文件的话,指定入口和输出文件路径即可完成一个迷你项目的打包。下面我们来通过一个简单的项目来看一下<code>Webpack</code>是怎样运行的。</p>
<h2>同步加载</h2>
<blockquote>本文使用 webpack ^4.30.0 作示例.为了更好地观察产出的文件,我们将模式设置为 development 关闭代码压缩,再开启 source-map 支持原始源代码调试。除此之外。我们还简单的写了一个插件<code>MyPlugin</code>来去除源码中的注释。</blockquote>
<p>新建<code>src/index.js</code></p>
<pre><code class="js">console.log('Hello webpack!');</code></pre>
<p>新建<code>webpack</code>配置文件<code>webpack.config.js</code></p>
<pre><code class="js">const path = require('path');
const MyPlugin = require('./src/MyPlugin.js')
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist')
},
plugins:[
new MyPlugin()
]
};</code></pre>
<p>新建<code>src/MyPlugin.js</code>。<a href="https://link.segmentfault.com/?enc=7Bcu%2FgRLn322HyvzHfddTQ%3D%3D.aHfS7TJhbu2wk6DFVre9ye5gb%2F%2BM0H5WhIf5%2BSkDTdUrFi4XLy0WALi6MGwiyRDf" rel="nofollow">了解webpack插件更多信息</a></p>
<pre><code class="js">class MyPlugin {
constructor(options) {
this.options = options
this.externalModules = {}
}
apply(compiler) {
var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g
compiler.hooks.emit.tap('CodeBeautify', (compilation)=> {
Object.keys(compilation.assets).forEach((data)=> {
let content = compilation.assets[data].source() // 欲处理的文本
content = content.replace(reg, function (word) { // 去除注释后的文本
return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
});
compilation.assets[data] = {
source(){
return content
},
size(){
return content.length
}
}
})
})
}
}
module.exports = MyPlugin</code></pre>
<p>现在我们运行命令 <code>webpack --config webpack.config.js</code> ,打包完成后会多出一个输出目录 <code>dist</code>:<code>dist/main.js</code>。<code>main</code> 是 <code>webpack </code>默认设置的输出文件名,我们快速瞄一眼这个文件:</p>
<pre><code class="js">(function(modules){
// ...
})({
"./src/index.js": (function(){
// ...
})
});</code></pre>
<p>整个文件只含一个立即执行函数<code>(IIFE)</code>,我们称它为 <code>webpackBootstrap</code>,它仅接收一个对象 —— 未加载的 模块集合(<code>modules</code>),这个<code> modules</code> 对象的 <code>key</code> 是一个路径,<code>value </code>是一个函数。你也许会问,这里的模块是什么?它们又是如何加载的呢?<br>在细看产出代码前,我们先丰富一下源代码:<br>新文件 <code>src/utils/math.js</code>:</p>
<pre><code class="js">export const plus = (a, b) => {
return a + b;
};</code></pre>
<p>修改<code>src/index.js</code>:</p>
<pre><code class="js">import { plus } from './utils/math.js';
console.log('Hello webpack!');
console.log('1 + 2: ', plus(1, 2));</code></pre>
<p>我们按照 ES 规范的模块化语法写了一个简单的模块 src/utils/math.js,给 src/index.js 引用。Webpack 用自己的方式支持了 ES6 Module 规范,前面提到的 module 就是和 ES6 module 对应的概念。</p>
<p>接下来我们看一下这些模块是如何通 ES5 代码实现的。再次运行命令 <code>webpack --config webpack.config.js</code> 后查看输出文件:</p>
<pre><code class="js">(function(modules){
// ...
})({
"./src/index.js": (function(){
// ...
}),
"./src/utils/math.js": (function() {
// ...
})
});</code></pre>
<p>IIFE 传入的 modules 对象里多了一个键值对,对应着新模块 src/utils/math.js,这和我们在源代码中拆分的模块互相呼应。然而,有了 modules 只是第一步,这份文件最终达到的效果应该是让各个模块按开发者编排的顺序运行。</p>
<h3>探究 webpackBootstrap</h3>
<p>接下来看看<code> webpackBootstrap</code> 函数中有些什么:</p>
<pre><code class="js">// webpackBootstrap
(function(modules){
// 缓存 __webpack_require__ 函数加载过的模块
var installedModules = {};
/**
* Webpack 加载函数,用来加载 webpack 定义的模块
* @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js"
* @returns {Object} exports 导出对象
*/
function __webpack_require__(moduleId) {
// ...
}
// 在 __webpack_require__ 函数对象上挂载一些变量及函数 ...
// 传入表达式的值为 "./src/index.js"
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})(/* modules */);</code></pre>
<p>可以看到其实主要做了两件事:</p>
<ol>
<li>定义一个模块加载函数 <code>__webpack_require__</code>。</li>
<li>使用加载函数加载入口模块 <code>"./src/index.js"</code>。</li>
</ol>
<p>整个 webpackBootstrap 中只出现了入口模块的影子,那其他模块又是如何加载的呢?我们顺着 __webpack_require__("./src/index.js") 细看加载函数的内部逻辑:</p>
<pre><code class="js">function __webpack_require__(moduleId) {
// 重复加载则利用缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 如果是第一次加载,则初始化模块对象,并缓存
var module = installedModules[moduleId] = {
i: moduleId, // 模块 ID
l: false, // 模块加载标识
exports: {} // 模块导出对象
};
/**
* 执行模块
* @param module.exports -- 模块导出对象引用,改变模块包裹函数内部的 this 指向
* @param module -- 当前模块对象引用
* @param module.exports -- 模块导出对象引用
* @param __webpack_require__ -- 用于在模块中加载其他模块
*/
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 模块加载标识置为已加载
module.l = true;
// 返回当前模块的导出对象引用
return module.exports;
}</code></pre>
<p>首先,加载函数使用了闭包变量 <code>installedModules</code>,用来将已加载过的模块保存在内存中。 接着是初始化模块对象,并把它挂载到缓存里。然后是模块的执行过程,加载入口文件时<code> modules[moduleId] </code>其实就是 <code>./src/index.js</code> 对应的模块函数。执行模块函数前传入了跟模块相关的几个实参,让模块可以导出内容,以及加载其他模块的导出。最后标识该模块加载完成,返回模块的导出内容。</p>
<p>根据 <code>__webpack_require__</code> 的缓存和导出逻辑,我们得知在整个<code> IIFE</code> 运行过程中,加载已缓存的模块时,都会直接返回<code>installedModules[moduleId].exports</code>,换句话说,相同的模块只有在第一次引用的时候才会执行模块本身。</p>
<h3>模块执行函数</h3>
<p><code>__webpack_require__ </code>中通过 <code>modules[moduleId].call()</code> 运行了模块执行函数,下面我们就进入到 <code>webpackBootstrap</code> 的参数部分,看看模块的执行函数。</p>
<pre><code class="js">/*** 入口模块 ./src/index.js ***/
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// 用于区分 ES 模块和其他模块规范,不影响理解 demo,战略跳过。
__webpack_require__.r(__webpack_exports__);
/* harmony import */
// 源模块代码中,`import {plus} from './utils/math.js';` 语句被 loader 解析转化。
// 加载 "./src/utils/math.js" 模块,
var _utils_math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils/math.js");
console.log('Hello webpack!');
console.log('1 + 2: ', Object(_utils_math_js__WEBPACK_IMPORTED_MODULE_0__["plus"])(1, 2));
}),
"./src/utils/math.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */
// 源模块代码中,`export` 语句被 loader 解析转化。
__webpack_require__.d(__webpack_exports__, "plus", function () {
return plus;
});
const plus = (a, b) => {
return a + b;
};
})</code></pre>
<p>执行顺序是:入口模块 -> 工具模块 -> 入口模块。入口模块中首先就通过 <code>__webpack_require__("./src/utils/math.js")</code> 拿到了工具模块的<code> exports </code>对象。再看工具模块,<code>ES </code>导出语法转化成了<code>__webpack_require__.d(__webpack_exports__, [key], [getter])</code>,而 <code>__webpack_require__.d</code> 函数的定义在 <code>webpackBootstrap</code> 内:</p>
<pre><code class="js">// 定义 exports 对象导出的属性。
__webpack_require__.d = function (exports, name, getter) {
// 如果 exports (不含原型链上)没有 [name] 属性,定义该属性的 getter。
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
enumerable: true,
get: getter
});
}
};
// 包装 Object.prototype.hasOwnProperty 函数。
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};</code></pre>
<p>可见<code> __webpack_require__.d</code> 其实就是 <code>Object.defineProperty</code> 的简单包装.<br>引用工具模块导出的变量后,入口模块再执行它剩余的部分。至此,Webpack 基本的模块执行过程就结束了。</p>
<p>好了,我们用流程图总结一下 Webpack 模块的加载思路:<br><img src="/img/bVbsgUx?w=836&h=1228" alt="图片描述" title="图片描述"></p>
<h2>异步加载</h2>
<p>有上面的打包我们发现将不同的打包进一个 <code>main.js </code>文件。<code>main.js</code> 会集中消耗太多网络资源,导致用户需要等待很久才可以开始与网页交互。</p>
<p>一般的解决方式是:根据需求降低首次加载文件的体积,在需要时(如切换前端路由器,交互事件回调)异步加载其他文件并使用其中的模块。</p>
<p>Webpack 推荐用 ES import() 规范来异步加载模块,我们根据 ES 规范修改一下入口模块的 import 方式,让其能够异步加载模块:</p>
<p><code>src/index.js</code></p>
<pre><code class="js">console.log('Hello webpack!');
window.setTimeout(() => {
import('./utils/math').then(mathUtil => {
console.log('1 + 2: ' + mathUtil.plus(1, 2));
});
}, 2000);
</code></pre>
<p>工具模块<code>(src/utils/math.js)</code>依然不变,在<code>webpack</code> 配置里,我们指定一下资源文件的公共资源路径<code>(publicPath)</code>,后面的探索过程中会遇到。</p>
<pre><code class="js">const path = require('path');
const MyPlugin = require('./src/MyPlugin.js')
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/'
},
plugins:[
new MyPlugin()
]
};
</code></pre>
<p>接着执行一下打包,可以看到除了<code> dist/main.js </code>外,又多了一个<code> dist/0.js</code> <code>./src/utils/math.js</code>。模块从<code>main chunk</code> 迁移到了<code> 0 chunk</code> 中。而与 <code>demo1 </code>不同的是,<code>main chunk </code>中添加了一些用于异步加载的代码,我们概览一下:</p>
<pre><code class="js">// webpackBootstrap
(function (modules) {
// 加载其他 chunk 后的回调函数
function webpackJsonpCallback(data) {
// ...
}
// ...
// 用于缓存 chunk 的加载状态,0 为已加载
var installedChunks = {
"main": 0
};
// 拼接 chunk 的请求地址
function jsonpScriptSrc(chunkId) {
// ...
}
// 同步 require 函数,内容不变
function __webpack_require__(moduleId) {
// ...
}
// 异步加载 chunk,返回封装加载过程的 promise
__webpack_require__.e = function requireEnsure(chunkId) {
// ...
}
// ...
// defineProperty 的包装,内容不变
__webpack_require__.d = function (exports, name, getter) {}
// ...
// 根据配置文件确定的 publicPath
__webpack_require__.p = "/dist/";
/**** JSONP 初始化 ****/
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
/**** JSONP 初始化 ****/
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
"./src/index.js": (function(module, exports, __webpack_require__) {
document.write('Hello webpack!\n');
window.setTimeout(() => {
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./utils/math */ "./src/utils/math.js")).then(mathUtil => {
console.log('1 + 2: ' + mathUtil.plus(1, 2));
});
}, 2000);
})
})</code></pre>
<p>可以看到 <code>webpackBootstrap</code> 的函数体部分增加了一些内容,参数部分移除了<code> "./src/utils/math.js"</code> 模块。跟着包裹函数的执行顺序,我们先聚焦到<code>「JSONP 初始化」</code>部分:</p>
<pre><code class="js">// 存储 jsonp 的数组,首次运行为 []
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 保存 jsonpArray 的 push 函数,首次运行为 Array.prototype.push
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 将 jsonpArray 的 push 重写为 webpackJsonpCallback (加载其他 chunk 后的回调函数)
jsonpArray.push = webpackJsonpCallback;
// 将 jsonpArray 重置为正常数组,push 重置为 Array.prototype.push
jsonpArray = jsonpArray.slice();
// 由于 jsonpArray 为 [],不做任何事
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// Array.prototype.push
var parentJsonpFunction = oldJsonpFunction;</code></pre>
<p>初始化结束后,变化就是 window 上挂载了一个 webpackJsonp 数组,它的值为 [];此外,<strong>这个数组的 push 被改写为 webpackJsonpCallback 函数</strong>,我们在后面会提到这些准备工作的作用。</p>
<p>接着是 <code>__webpack_require__ </code>入口模块,由于<code> __webpack_require__</code> 函数没有改变,我们继续观察入口模块执行函数有了什么变化。</p>
<p>显然,<code>import('../utils/math.js') </code>被转化为<code>__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/utils/math.js"))</code>。0 是<code> ./src/utils/math.js </code>所在<code> chunk </code>的<code>id</code>,「同步加载模块」的逻辑拆分成了「先加载<code> chunk</code>,完成后再加载模块」。</p>
<p>我们翻到 <code>__webpack_require__.e </code>的定义位置:</p>
<pre><code class="js">__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// installedChunks 是在 webpackBootstrap 中维护的 chunk 缓存
var installedChunkData = installedChunks[chunkId];
// chunk 未加载
if(installedChunkData !== 0) {
// installedChunkData 为 promise 表示 chunk 加载中
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
/*** 首次加载 chunk: ***/
// 初始化 promise 对象
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 创建 script 标签加载 chunk
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
var onScriptComplete;
// ... 省略一些 script 属性设置
// src 根据 publicPath 和 chunkId 拼接
script.src = jsonpScriptSrc(chunkId);
// 加载结束回调函数,处理 script 加载完成、加载超时、加载失败的情况
onScriptComplete = function (event) {
script.onerror = script.onload = null; // 避免 IE 内存泄漏问题
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// 处理 script 加载完成,但 chunk 没有加载完成的情况
if(chunk !== 0) {
// chunk 加载中
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
// reject(error)
chunk[1](error);
}
// 统一将没有加载的 chunk 标记为未加载
installedChunks[chunkId] = undefined;
}
};
// 设置 12 秒超时时间
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
head.appendChild(script);
/*** 首次加载 chunk ***/
}
}
return Promise.all(promises);
};</code></pre>
<p>看起来有点长,我们一步步剖析,先从第一行和最后一行来看,整个函数将异步加载的过程封装到了 promise 中,最终导出。</p>
<p>接着从第二行开始,<code>installedChunkData</code> 从缓存中取值,显然首次加载 chunk 时此处是 undefined。接下来,<code>installedChunkData</code> 的 <code>undefined </code>值触发了第一层 if 语句的判断条件。紧接着进行到第二层 if 语句,此时根据判断条件走入 <code>else</code> 块,这里<code> if</code> 块里的内容我们先战略跳过,else 里主要有两块内容,一是 chunk 脚本加载过程,这个过程创建了一个 <code>script</code> 标签,使其请求 <code>chunk</code>所在地址并执行 <code>chunk </code>内容;二是初始化 <code>promise</code> ,并用 <code>promise</code> 控制 <code>chunk </code>文件加载过程。</p>
<p>不过,我们只在这段 <code>else</code> 代码块中找到了 <code>reject</code> 的使用处,也就是在 <code>chunk</code> 加载异常时 <code>chunk[1](error)</code> 的地方,但并没发现更重要的 <code>resolve</code> 的使用地点,仅仅是把 <code>resolve</code> 挂在了缓存上<code>(installedChunks[chunkId] = [resolve, reject])</code>。</p>
<p>这里的<code> chunk</code> 文件加载下来会发生什么呢?让我们打开<code>dist/0.js</code> 一探究竟:</p>
<pre><code class="js">(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], {
"./src/utils/math.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "plus", function () {
return plus;
});
const plus = (a, b) => {
return a + b;
};
})
}]);
</code></pre>
<p>我们发现了:</p>
<ol>
<li>久违的 ./src/utils/math.js 模块</li>
<li>window["webpackJsonp"] 数组的使用地点</li>
</ol>
<p>这段代码开始执行,把异步加载相关的 <code>chunk id </code>与模块传给<code> push</code> 函数。而前面已经提到过,<code>window["webpackJsonp"]</code> 数组的 push 函数已被重写为<code> webpackJsonpCallback </code>函数,它的定义位置在 <code>webpackBootstrap</code> 中:</p>
<pre><code class="js">function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
// 将 chunk 标记为已加载
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 把 "moreModules" 加到 webpackBootstrap 中的 modules 闭包变量中。
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// parentJsonpFunction 是 window["webpackJsonp"] 的原生 push
// 将 data 加入全局数组,缓存 chunk 内容
if(parentJsonpFunction) parentJsonpFunction(data);
// 执行 resolve 后,加载 chunk 的 promise 状态变为 resolved,then 内的函数开始执行。
while(resolves.length) {
resolves.shift()();
}
};</code></pre>
<p>走进这个函数中,意味着异步加载的 chunk 内容已经拿到,这个时候我们要完成两件事,一是让依赖这次异步加载结果的模块继续执行,二是缓存加载结果。</p>
<p>关于第一点,我们回忆一下之前 <code>__webpack_require__.e</code> 的内容,此时 <code>chunk</code> 还处于「加载中」的状态,也就是说对应的<code> installedChunks[chunkId]</code> 的值此时为<code> [resolve, reject, promise]</code>。 而这里,chunk 已经加载,但 <code>promise</code> 还未决议,于是 <code>webpackJsonpCallback</code> 内部定义了一个 <code>resolves</code> 变量用来收集 <code>installedChunks</code> 上的<code> resolve</code> 并执行它。</p>
<p>接下来说到第二点,就要涉及几个层面的缓存了。</p>
<p>首先是 chunk 层面,这里有两个相关操作,操作一将 <code>installedChunks[chunkId]</code> 置为 0 可以让 <code>__webpack_require__.e </code>在第二次加载同一 <code>chunk </code>时返回一个立即决议的 <code>promise(Promise.all([]))</code>;操作二将 <code>chunk data </code>添加进<code> window["webpackJsonp"]</code> 数组,可以在多入口模式时,方便地拿到已加载过的 <code>chunk </code>缓存。通过以下代码实现:</p>
<pre><code class="js">/*** 缓存执行部分 ***/
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// ...
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
/*** 缓存执行部分 ***/
/*** 缓存添加部分 ***/
function webpackJsonpCallback(data) {
//...
// 此处的 parentJsonpFunction 是 window["webpackJsonp"] 数组的原生 push
if (parentJsonpFunction) parentJsonpFunction(data);
//...
}
/*** 缓存添加部分 ***/</code></pre>
<p>而在 modules 层面,<code>chunk</code> 中的 <code>moreModules</code> 被合入入口文件的 <code>modules</code> 中,可供下一个微任务中的<code> __webpack_require__ </code>同步加载模块。</p>
<pre><code class="js">
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
console.log('Hello webpack!');
window.setTimeout(() => {
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/utils/math.js")).then(mathUtil => {
console.log('1 + 2: ' + mathUtil.plus(1, 2));
});
}, 2000);
})
});</code></pre>
<p><code>__webpack_require__.e(0)</code> 返回的 promise 决议后,<code>__webpack_require__.bind(null, "./src/utils/math.js")</code> 可以加载到 <code>chunk </code>携带的模块,并返回模块作为下一个微任务函数的入参,接下来就是<code> Webpack Loader</code> 翻译过的其他业务代码了。</p>
<p>现在让我们把异步流程梳理一下:<br><img src="/img/bVbsm8n?w=720&h=1259" alt="图片描述" title="图片描述"></p>
手写一个webpack插件
https://segmentfault.com/a/1190000019010101
2019-04-28T08:38:22+08:00
2019-04-28T08:38:22+08:00
Alan
https://segmentfault.com/u/xc_xiang
32
<blockquote>本文示例源代码请戳<a href="https://link.segmentfault.com/?enc=lVwyf2eVpHo%2BNMgsW2OHZw%3D%3D.brOC6TMZPMSLQAPtq3ogidLWS14HhAxYVbf2NzzRSLu%2B4azp6JkBR%2FvCpevp8EuIhYIqxq3C25hqgqJk8i%2F%2Fn7T0CMuxt3bymk0SD7gnH3k%3D" rel="nofollow">github博客</a>,建议大家动手敲敲代码。</blockquote>
<p>webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。Tapable暴露出挂载plugin的方法,使我们能 将plugin控制在webapack事件流上运行(如下图)。<br><img src="https://segmentfault.com/img/bVbrUZm" alt="图片描述" title="图片描述"></p>
<h2>Tabable是什么?</h2>
<p><a href="https://link.segmentfault.com/?enc=6XUL8sgFG5fV9PnN%2FH2ekQ%3D%3D.ydCXcGNiyLk6ogdfz4GIrqM1i%2FLNzy2znfAyrYs7aDADiH5l9tg%2BYg8m1HmiR0wT" rel="nofollow">tapable</a>库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。</p>
<pre><code class="js">const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
</code></pre>
<p><img src="https://segmentfault.com/img/bVbrU0E" alt="图片描述" title="图片描述"></p>
<p><strong>Tabable 用法</strong></p>
<p>1.new Hook 新建钩子</p>
<ul>
<li>tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。</li>
<li>class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。</li>
</ul>
<pre><code class="js">const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);</code></pre>
<p>2.使用 <code>tap/tapAsync/tapPromise</code> 绑定钩子<br>tapable提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。</p>
<table>
<thead><tr>
<th>-</th>
<th>Async*</th>
<th>Sync*</th>
</tr></thead>
<tbody>
<tr>
<td>绑定</td>
<td><code>tapAsync/tapPromise/tap</code></td>
<td><code>tap</code></td>
</tr>
<tr>
<td>执行</td>
<td><code>callAsync/promise</code></td>
<td><code>call</code></td>
</tr>
</tbody>
</table>
<p>3.<code>call/callAsync</code> 执行绑定事件</p>
<pre><code class="js">const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
//绑定事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
//执行绑定的事件
hook1.call(1,2,3)</code></pre>
<p>举个例子</p>
<ul>
<li>定义一个<code>Car</code>方法,在内部<code>hooks</code>上新建钩子。分别是同步钩子 <code>accelerate</code>(<code>accelerate</code>接受一个参数)、<code>break</code>、异步钩子<code>calculateRoutes</code>
</li>
<li>使用钩子对应的绑定和执行方法</li>
<li>
<code>calculateRoutes</code>使用<code>tapPromise</code>可以返回一个<code>promise</code>对象。</li>
</ul>
<pre><code class="js">//引入tapable
const { SyncHook, AsyncParallelHook } = require('tapable');
//创建类
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
}
const myCar = new Car();
//绑定同步钩子
myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
// return a promise
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source} ${target} ${routesList}`)
resolve();
},1000)
})
});
//执行同步钩子
myCar.hooks.break.call();
myCar.hooks.accelerate.call('hello');
console.time('cost');
//执行异步钩子
myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
console.timeEnd('cost');
}, err => {
console.error(err);
console.timeEnd('cost');
})</code></pre>
<p>运行结果</p>
<pre><code class="js">WarningLampPlugin
Accelerating to hello
tapPromise to i love tapable
cost: 1008.725ms</code></pre>
<p><code>calculateRoutes</code>也可以使用<code>tapAsync</code>绑定钩子,注意:此时用<code>callback</code>结束异步回调。</p>
<pre><code class="js">myCar.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
// return a promise
setTimeout(() => {
console.log(`tapAsync to ${source} ${target} ${routesList}`)
callback();
}, 2000)
});
myCar.hooks.calculateRoutes.callAsync('i', 'like', 'tapable', err => {
console.timeEnd('cost');
if(err) console.log(err)
})</code></pre>
<p>运行结果</p>
<pre><code class="js">WarningLampPlugin
Accelerating to hello
tapAsync to i like tapable
cost: 2007.045ms</code></pre>
<p><strong>进阶一下~</strong><br>到这里可能已经学会使用tapable了,但是它如何与<code>webapck/webpack</code>插件关联呢?<br>我们将刚才的代码稍作改动,拆成两个文件:<code>Compiler.js</code>、<code>Myplugin.js</code></p>
<p><code>Compiler.js</code></p>
<ul>
<li>把<code>Class Car</code>类名改成<code>webpack</code>的核心<code>Compiler</code>
</li>
<li>接受<code>options</code>里传入的<code>plugins</code>
</li>
<li>将<code>Compiler</code>作为参数传给<code>plugin</code>
</li>
<li>执行<code>run</code>函数,在编译的每个阶段,都触发执行相对应的钩子函数。</li>
</ul>
<pre><code class="js">const {
SyncHook,
AsyncParallelHook
} = require('tapable');
class Compiler {
constructor(options) {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
}
run(){
console.time('cost');
this.accelerate('hello')
this.break()
this.calculateRoutes('i', 'like', 'tapable')
}
accelerate(param){
this.hooks.accelerate.call(param);
}
break(){
this.hooks.break.call();
}
calculateRoutes(){
const args = Array.from(arguments)
this.hooks.calculateRoutes.callAsync(...args, err => {
console.timeEnd('cost');
if (err) console.log(err)
});
}
}
module.exports = Compiler
</code></pre>
<p>MyPlugin.js</p>
<ul>
<li>引入<code>Compiler</code>
</li>
<li>定义一个自己的插件。</li>
<li>
<code>apply</code>方法接受 <code>compiler</code>参数。</li>
<li>给<code>compiler</code>上的钩子绑定方法。</li>
<li>仿照<code>webpack</code>规则,向 <code>plugins</code> 属性传入<code> new </code>实例。</li>
</ul>
<blockquote>
<code>webpack</code> 插件是一个具有<code> apply</code> 方法的 <code>JavaScript</code> 对象。<code>apply</code> 属性会被 <code>webpack compiler </code>调用,并且 <code>compiler</code> 对象可在整个编译生命周期访问。</blockquote>
<pre><code class="js">const Compiler = require('./Compiler')
class MyPlugin{
constructor() {
}
apply(conpiler){//接受 compiler参数
conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`)
callback();
}, 2000)
});
}
}
//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()
</code></pre>
<p>运行结果</p>
<pre><code class="js">Accelerating to hello
WarningLampPlugin
tapAsync to iliketapable
cost: 2009.273ms</code></pre>
<p>改造后运行正常,仿照Compiler和webpack插件的思路慢慢得理顺插件的逻辑成功。<br>更多其他<a href="https://link.segmentfault.com/?enc=A684Dy3L6ewk9Z5aAzi9RA%3D%3D.FgJ2b%2F4OQafUyd9Q3iT96IIMuHAH3O%2BL9Hlz8emVjiiMp4eZ9iSB0mqiOEMzNfnJ" rel="nofollow">Tabable方法</a></p>
<h2>Plugin基础</h2>
<p><code>Webpack </code>通过 <code>Plugin</code> 机制让其更加灵活,以适应各种应用场景。 在 <code>Webpack</code> 运行的生命周期中会广播出许多事件,<code>Plugin </code>可以监听这些事件,在合适的时机通过 <code>Webpack </code>提供的 <code>API</code> 改变输出结果。</p>
<p>一个最基础的 Plugin 的代码是这样的:</p>
<pre><code class="js">class BasicPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}
// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
compiler.hooks.compilation.tap('BasicPlugin', compilation => {
});
}
}
// 导出 Plugin
module.exports = BasicPlugin;
</code></pre>
<p>在使用这个 Plugin 时,相关配置代码如下:</p>
<pre><code class="js">const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins:[
new BasicPlugin(options),
]
}</code></pre>
<p><strong>Compiler 和 Compilation</strong><br>在开发 <code>Plugin </code>时最常用的两个对象就是 <code>Compiler </code>和 <code>Compilation</code>,它们是 <code>Plugin </code>和 <code>Webpack </code>之间的桥梁。<code> Compiler</code> 和 <code>Compilation</code> 的含义如下:</p>
<ul>
<li>Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;</li>
<li>Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。</li>
</ul>
<p>Compiler 和 Compilation 的<strong>区别在于</strong>:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。</p>
<h3>常用 API</h3>
<p>插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。 由于 Webpack 提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面来介绍一些常用的 API。</p>
<p>1、读取输出资源、代码块、模块及其依赖</p>
<p>有些插件可能需要读取 Webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。</p>
<p>在 emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:</p>
<pre><code class="js">class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tabAsync('MyPlugin', (compilation, callback) => {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});
// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}</code></pre>
<p>2、监听文件变化</p>
<p>Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。</p>
<p>在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:</p>
<pre><code class="js">// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});</code></pre>
<p>默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:</p>
<pre><code class="js">compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => {
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
});</code></pre>
<p>3、修改输出资源<br>有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 <code>emit</code> 事件,因为发生 <code>emit</code> 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此<code> emit </code>事件是修改 <code>Webpack </code>输出资源的最后时机。</p>
<p>所有需要输出的资源会存放在 <code>compilation.assets </code>中,<code>compilation.assets </code>是一个键值对,键为需要输出的文件名称,值为文件对应的内容。</p>
<p>设置 <code>compilation.assets </code>的代码如下:</p>
<pre><code class="js">// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();</code></pre>
<p>读取 <code>compilation.assets </code>的代码如下:</p>
<pre><code class="js"> // 读取名称为 fileName 的输出资源
const asset = compilation.assets[fileName];
// 获取输出资源的内容
asset.source();
// 获取输出资源的文件大小
asset.size();
callback();</code></pre>
<h2>实战!写一个插件</h2>
<p>怎么写一个插件?参照webpack官方教程<a href="https://link.segmentfault.com/?enc=y6YKA7KQk7JhnQNOOd1tsA%3D%3D.lwlwMoBj0PDhmyXjEKVKCoXEZUkTsYIygNvCanbL5P1NJuturEUdo6IxREAOT5ZyNSrzmM0a2HYJmtkncaGqX%2FRf0EqsbBi5KybsF1c2osw%3D" rel="nofollow">Writing a Plugin</a>。 一个webpack plugin由一下几个步骤组成:</p>
<ul>
<li>一个JavaScript类函数。</li>
<li>在函数原型 (prototype)中定义一个注入compiler对象的apply方法。</li>
<li>apply函数中通过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象</li>
<li>使用compilation操纵修改webapack内部实例数据。</li>
<li>异步插件,数据处理完后使用callback回调</li>
</ul>
<p>下面我们举一个实际的例子,带你一步步去实现一个插件。<br>该插件的名称取名叫 <code>EndWebpackPlugin</code>,作用是在 <code>Webpack</code> 即将退出时再附加一些额外的操作,例如在 <code>Webpack</code> 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。 同时该插件还能区分<code> Webpack</code> 构建是否执行成功。使用该插件时方法如下:</p>
<pre><code class="js">module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err);
})
]
}</code></pre>
<p>要实现该插件,需要借助两个事件:</p>
<ul>
<li>
<code>done</code>:在成功构建并且输出了文件后,Webpack 即将退出时发生;</li>
<li>
<code>failed</code>:在构建出现异常导致构建失败,Webpack 即将退出时发生;</li>
</ul>
<p>实现该插件非常简单,完整代码如下:</p>
<pre><code class="js">class EndWebpackPlugin {
constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.hooks.done.tab('EndWebpackPlugin', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
compiler.hooks.failed.tab('EndWebpackPlugin', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;</code></pre>
<p>从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。 在 工作原理概括 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。</p>
<p>参考<br><a href="https://link.segmentfault.com/?enc=6JeCq%2BF9U55oDpcT6Bj0oA%3D%3D.395OkY7pN177ERGtXvlfUukon9ZS3jS6eOZE9zniEi3uTpsaqsT2Z6cA1IPzbSPy" rel="nofollow">tapable</a><br><a href="https://link.segmentfault.com/?enc=9Cg17C8Dox4BsNa3pMkyWQ%3D%3D.JeLiWMshDPWr682XkEYgY4xX%2FvsZeTyJsGiaRpFJOy4ltpUzBUMfRhu7nujOikSb" rel="nofollow">compiler-hooks</a><br><a href="https://link.segmentfault.com/?enc=oQ7e1rf%2Bc0r6n7AoYAzFYw%3D%3D.5wiOpNwTCfIiCqGF26HJCo5Ho%2F8J5usDOQsj6ftievW5UxaXa2DYlq8EuojMxYGR" rel="nofollow">Compilation Hooks</a><br><a href="https://link.segmentfault.com/?enc=upYK0zTmszmzv33FHWDWHA%3D%3D.A5EyQu8RWXMd0G9dqyqaRDG0v1YEO87rTCoifcD9WwoCFFxivVmSoyta%2BOIs%2BnnzOtCSVJGJC%2B3m6q0EgEvmfQ%3D%3D" rel="nofollow">writing-a-plugin</a><br><a href="https://link.segmentfault.com/?enc=NJf%2BL0dMTrgyjB7UMpY%2BqQ%3D%3D.SO83wh%2F8xPz29Qdpbs0sB47yqK3amj0qUZO6lDUtHPY%3D" rel="nofollow">深入浅出 Webpack</a><br><a href="https://link.segmentfault.com/?enc=i9%2FFk0tzOD6rtzFFBwmEIQ%3D%3D.BYu%2FVyDFSd7g%2BYjY2kW0NhmpstdzNXRgs1vrxj%2BsepcGrgsL3243KeYSXqSDg%2BNJ" rel="nofollow">干货!撸一个webpack插件</a></p>
手把手教你写一个 Webpack Loader
https://segmentfault.com/a/1190000018980814
2019-04-25T08:55:31+08:00
2019-04-25T08:55:31+08:00
Alan
https://segmentfault.com/u/xc_xiang
41
<p>本文示例源代码请戳<a href="https://link.segmentfault.com/?enc=SdapPNuCLRsxQxfhE44Axw%3D%3D.EQAOYrzcq%2FQRVSKSeU2GOIuc8v00I2UsH2oro9U%2BcIr24TGvrNrB6tPuq9l04EsG5I4a%2FXyTgbPBmfQRZugoQbZjlT996XMwYbxhEZe8PeU%3D" rel="nofollow">github博客</a>,建议大家动手敲敲代码。</p>
<blockquote>本文不会介绍loader的一些使用方法,不熟悉的同学请自行查看<a href="https://link.segmentfault.com/?enc=lKlRWsygmAhGrrgHfHm7jw%3D%3D.D5sqNHXKORlG2PvTc4O%2FuXsaPg9pply9VS0p8gElsrwpUnMZfHvNcDpiIpMESqua" rel="nofollow">Webpack loader</a>
</blockquote>
<h2>1、背景</h2>
<p>首先我们来看一下为什么需要<code>loader</code>,以及他能干什么?<br><code>webpack</code> 只能理解 <code>JavaScript</code> 和 <code>JSON</code> 文件。<code>loader</code> 让 <code>webpack</code> 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。</p>
<p>本质上来说,<code>loader</code> 就是一个 <code>node</code> 模块,这很符合 <code>webpack</code> 中「万物皆模块」的思路。既然是 <code>node</code> 模块,那就一定会导出点什么。在 <code>webpack</code> 的定义中,<code>loader</code> 导出一个函数,<code>loader</code> 会在转换源模块<code>resource</code>的时候调用该函数。在这个函数内部,我们可以通过传入 <code>this </code>上下文给 <a href="https://link.segmentfault.com/?enc=a%2BvETLm1%2BiqKd1dU1Ajdtw%3D%3D.cG26D9%2FFIbWLVFYwVgUvFTUFinWwL79TA%2FsT1ECuNF99pzty1fQVKJ7mP7mLzrJ1" rel="nofollow">Loader API</a> 来使用它们。最终装换成可以直接引用的模块。</p>
<h2>2、xml-Loader 实现</h2>
<p>前面我们已经知道,由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。<br>一个简单的loader源码如下</p>
<pre><code class="js">module.exports = function(source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
return source;
};</code></pre>
<p>由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:</p>
<pre><code class="js">
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
module.exports = function(source) {
this.cacheable && this.cacheable();
const self = this;
parser.parseString(source, function (err, result) {
self.callback(err, !err && "module.exports = " + JSON.stringify(result));
});
};</code></pre>
<p>这里我们事简单实现一个<code>xml-loader</code>;</p>
<blockquote>注意:如果是处理顺序排在最后一个的 <code>loader</code>,那么它的返回值将最终交给 <code>webpack</code> 的 <code>require</code>,换句话说,它一定是一段可执行的<code> JS</code> 脚本 (用字符串来存储),更准确来说,是一个 <code>node</code> 模块的 <code>JS </code>脚本,所以我们需要用<code>module.exports =</code>导出。</blockquote>
<p>整个过程相当于这个 loader 把源文件</p>
<pre><code class="js">// 这里是 source 模块</code></pre>
<p>转化为</p>
<pre><code class="js">// example.js
module.exports = '这里是 source 模块';</code></pre>
<p>然后交给 require 调用方:</p>
<pre><code class="js">// applySomeModule.js
var source = require('example.js');
console.log(source); // 这里是 source 模块
</code></pre>
<p>写完后我们要怎么在本地验证呢?下面我们来写个简单的demo进行验证。</p>
<h3>2.1、验证</h3>
<p>首先我们创建一个根目录xml-loader,此目录下 npm init -y生成默认的package.json文件 ,在文件中配置打包命令</p>
<pre><code class="js">"scripts": {
"dev": "webpack-dev-server"
},</code></pre>
<p>之后<code>npm i -D webpack webpack-cli</code>,安装完<code>webpack</code>,在根目录 创建配置文件<code>webpack.config.js</code></p>
<pre><code class="js">const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.xml$/,
use: ['xml-loader'],
}
]
},
resolveLoader: {
modules: [path.join(__dirname, '/src/loader')]
},
devServer: {
contentBase: './dist',
overlay: {
warnings: true,
errors: true
},
open: true
}
}
</code></pre>
<p>在根目录创建一个src目录,里面创建index.js,</p>
<pre><code class="js">import data from './foo.xml';
function component() {
var element = document.createElement('div');
element.innerHTML = data.note.body;
element.classList.add('header');
console.log(data);
return element;
}
document.body.appendChild(component());</code></pre>
<p>同时还有一个<code>foo.xml</code>文件</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder dd</heading>
<body>Call Cindy on Tuesday dd</body>
</note></code></pre>
<p>最后把上面的<code>xml-loader</code>放到<code>src/loader</code>文件夹下。<br>完整的<a href="https://link.segmentfault.com/?enc=cVwJImb0%2Fbbw%2BPKbg2b5cg%3D%3D.elCfijet5%2Bmvk9AUCoNeYMjE2ul1TLPCI%2Bti1wq2XrZpoj5BmFSfLnOwN9pkrSVSDJmxMFnVxTNwXCSEarz5aBufMBJ5%2BtJ7cykvciggm3w%3D" rel="nofollow">demo源码请看</a><br>最终我们的运行效果如下图<br><img src="https://segmentfault.com/img/bVbrNrY" alt="图片描述" title="图片描述"></p>
<p>至此一个简单的<code>webpack loader</code>就实现完成了。当然最终使用你可以发布到npm上。</p>
<h2>3、一些议论知识补充</h2>
<h3>3.1、获得 Loader 的 options</h3>
<p>当我们配置loader时我们经常会看到有这样的配置</p>
<pre><code class="js">ules: [{
test: /\.html$/,
use: [ {
loader: 'html-loader',
options: {
minimize: true
}
}],
}]</code></pre>
<p>那么我们在loader中怎么获取这写配置信息呢?答案是<code>loader-utils</code>。这个由webpack提供的工具。下面我们来看下使用方法</p>
<pre><code class="js">const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};
</code></pre>
<p>没错就是这么简单。</p>
<h3>3.2、加载本地 Loader</h3>
<p>1、path.resolve<br>可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件</p>
<pre><code class="js">{
test: /\.js$/
use: [
{
loader: path.resolve('path/to/loader.js'),
options: {/* ... */}
}
]
}</code></pre>
<p>2、ResolveLoader<br>这个就是上面我用到的方法。<code>ResolveLoader</code> 用于配置 <code>Webpack</code> 如何寻找 <code>Loader</code>。 默认情况下只会去 node_modules 目录下寻找,为了让 <code>Webpack</code> 加载放在本地项目中的 <code>Loader</code> 需要修改 <code>resolveLoader.modules</code>。<br>假如本地的 <code>Loader</code> 在项目目录中的 <code>./loaders/loader-name</code> 中,则需要如下配置:</p>
<pre><code class="js">module.exports = {
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}</code></pre>
<p>加上以上配置后, <code>Webpack</code> 会先去 <code>node_modules</code> 项目下寻找 <code>Loader</code>,如果找不到,会再去 <code>./loaders/ </code>目录下寻找。<br>3、<a href="https://link.segmentfault.com/?enc=n5OV9oCZ6IUuZHWMcKkQOA%3D%3D.UTcEzX8VXOJdxwsYv3Fg0AgoZ2RcpBARCzsOtyoE3hk%3D" rel="nofollow">npm link</a><br><code>npm link</code> 专门用于开发和调试本地 <code>npm</code> 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 <code>node_modules </code>目录下,让项目可以直接使用本地的 <code>npm</code> 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。</p>
<p>完成 <code>npm link </code>的步骤如下:</p>
<ul>
<li>确保正在开发的本地 npm 模块(也就是正在开发的 Loader)的 <code>package.json </code>已经正确配置好;</li>
<li>在本地 <code>npm </code>模块根目录下执行 <code>npm link</code>,把本地模块注册到全局;</li>
<li>在项目根目录下执行 <code>npm link loader-name</code>,把第2步注册到全局的本地 Npm 模块链接到项目的 <code>node_moduels</code> 下,其中的 <code>loader-name </code>是指在第1步中的<code>package.json </code>文件中配置的模块名称。</li>
</ul>
<p>链接好<code> Loader</code> 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的<code> Loader</code> 了。(npm link不是很熟,复制被人的)</p>
<h3>3.3、缓存加速</h3>
<p>在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。</p>
<p>如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:</p>
<pre><code class="js">module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};</code></pre>
<h3>3.4、处理二进制数据</h3>
<p>在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:</p>
<pre><code class="js">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;
</code></pre>
<p>以上代码中最关键的代码是最后一行<code> module.exports.raw = true;</code>,没有该行 Loader 只能拿到字符串。</p>
<h3>3.5、同步与异步</h3>
<p>Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。</p>
<p>在转换步骤是异步时,你可以这样:</p>
<pre><code class="js">module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};</code></pre>
<p><strong>参考</strong></p>
<p><a href="https://link.segmentfault.com/?enc=uyVZI1Xfr1xzKRBVi8NslQ%3D%3D.xq10QGB1GlKbinWM7fHVRI5u8e6%2Ba7%2BN989DVgClcqsw1v2j3SDdBoqSJuzKn94%2FARRaX1ohc%2FwkSiCjNWcsBQ%3D%3D" rel="nofollow">编写一个webpack loader</a></p>
LocalStorage
https://segmentfault.com/a/1190000018954001
2019-04-23T08:40:36+08:00
2019-04-23T08:40:36+08:00
Alan
https://segmentfault.com/u/xc_xiang
13
<blockquote>先来几道面试题<br>1、<code>a.meituan.com</code> 和 <code>b.meituan.com</code> 这两个域能够共享同一个 <code>localStorage</code> 吗?<br>2、在 <code>webview</code> 中打开一个页面:<code>i.meituan.com/home.html</code>,点击一个按钮,调用 <code>js</code> 桥打开一个新的 <code>webview:i.meituan.com/list.html</code>,这两个分属不同<code>webview</code>的页面能共享同一个 <code>localStorage</code> 吗?<br>3、如果 <code>localStorage</code> 存满了,再往里存东西,或者要存的东西超过了剩余容量,会发生什么?</blockquote>
<p>好了带着这些问题我们来往下看</p>
<h2>1、基本方法</h2>
<pre><code class="js">// 用于存入数据
window.localStorage.setItem('key', 'value');
// 用于读取数据
window.localStorage.getItem('key')
//清除某个键名对应的键值
localStorage.removeItem('key');
// 用于清除所有保存的数据
window.localStorage.clear()
// localStorage.key()接受一个整数作为参数(从零开始),返回该位置对应的键值。
localStorage.key(0)
// Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。利用这个可以实现跨tab页通信
window.addEventListener('storage', onStorageChange);</code></pre>
<p><img src="/img/bVbrAgu?w=922&h=391" alt="图片描述" title="图片描述"></p>
<blockquote>注意点:<code>localStorage</code>只能存String类型的字符串。存对象的时候会变成<code>"[object Object]"</code>,因为<code>({key:'xxx'}).toString()//"[object Object]"</code>。这个时候我们可以通过<code>JSON.stringify()</code>。来帮我们实现转化。例如:<code>localStorage.setItem('jsonString', JSON.stringify({key: 'mtt'}))</code>
</blockquote>
<h2>2、作用域</h2>
<p><code>localStorage</code>只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份<code>localStorage</code>数据。这就回答了我们上面的前二个问题了,<br>第一题:由于域名不同,不能进行共享。<br>第二题:二个<code>webview</code>相当于同一个浏览器的不同标签页。所以可以共享。</p>
<p>与<code>sessionStorage </code>、<code>cookie </code>对比</p>
<ul>
<li>
<code>localstorage</code>在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在</li>
<li>
<code>sessionStorage</code>:不能在不同的浏览器窗口中共享,即使是同一个页面;</li>
<li>
<code>cookie</code>: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在</li>
</ul>
<h2>3、数据存储有效期</h2>
<p><code>localStorage</code>理论上来说是永久有效的,即不主动清空的话就不会消失,即使保存的数据超出了浏览器所规定的大小,<strong>也不会把旧数据清空而只会报错</strong>(这里解答了上面的第三题)。但需要注意的是,在移动设备上的浏览器或各<code>Native App</code>用到的<code>WebView</code>里,<code>localStorage</code>都是不可靠的,可能会因为各种原因(比如说退出App、网络切换、内存不足等原因)被清空。 </p>
<p>与<code>sessionStorage </code>、<code>cookie </code>对比</p>
<ul>
<li>localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;</li>
<li>sessionStorage:仅在当前浏览器窗口关闭之前有效;</li>
<li>cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭</li>
</ul>
<h2>4、数据存储方面</h2>
<ul>
<li>
<code>sessionStorage</code>和<code>localStorage</code>不会自动把数据发送给服务器,仅在<strong>本地保存</strong>。</li>
<li>
<code>cookie</code>数据始终在同源的<code>http</code>请求中携带(即使不需要),即<code>cookie</code>在浏览器和服务器间来回传递。<code>cookie</code>数据还有路径<code>(path)</code>的概念,可以限制<code>cookie</code>只属于某个路径下</li>
</ul>
<p>$ 5、存储数据大小</p>
<ul>
<li>
<code>cookie</code>数据不能超过<code>4K</code>,同时因为每次<code>http</code>请求都会携带<code>cookie</code>、所以<code>cookie</code>只适合保存很小的数据,如会话标识。</li>
<li>
<code>sessionStorage</code>和<code>localStorage</code>虽然也有存储大小的限制,但比<code>cookie</code>大得多,可以达到5M或更大</li>
</ul>
<blockquote>Web Storage拥有setItem、getItem、removeItem、clear等方法,不像cookie需要自己封装setCookie、getCookie等方法</blockquote>
浏览器渲染机制
https://segmentfault.com/a/1190000018917730
2019-04-19T09:24:21+08:00
2019-04-19T09:24:21+08:00
Alan
https://segmentfault.com/u/xc_xiang
35
<blockquote>本文示例源代码请戳<a href="https://link.segmentfault.com/?enc=%2FhcY7X7GrhJRfdwfZb91Uw%3D%3D.KURTRTpMhWLmHyl64%2FlE6GIlCMxtJ4NMv3lixOkfnNgHcAYYtg2d7PwALB27aabQPw5QlYOXsxtoIGs890ymag%3D%3D" rel="nofollow">github</a>博客,建议大家动手敲敲代码。</blockquote>
<h2>前言</h2>
<p>浏览器渲染页面的过程</p>
<p>从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:</p>
<ol>
<li>DNS 查询</li>
<li>TCP 连接</li>
<li>HTTP 请求即响应</li>
<li>服务器响应</li>
<li>客户端渲染</li>
</ol>
<p>本文讨论第五个部分,即浏览器对内容的渲染,这一部分(渲染树构建、布局及绘制),又可以分为下面五个步骤:</p>
<ol>
<li>处理 HTML 标记并构建 DOM 树。</li>
<li>处理 CSS 标记并构建 CSSOM 树</li>
<li>将 DOM 与 CSSOM 合并成一个渲染树。</li>
<li>根据渲染树来布局,以计算每个节点的几何信息。</li>
<li>将各个节点绘制到屏幕上。</li>
</ol>
<p>需要明白,这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。</p>
<h2>1、浏览器的线程</h2>
<p>在详细说明之前我们来看一下浏览器线程。这将有助于我们理解后续内容。</p>
<p>浏览器是多线程的,它们在内核制控下相互配合以保持同步。一个浏览器至少实现三个常驻线程:JavaScript 引擎线程,GUI 渲染线程,浏览器事件触发线程。</p>
<ul>
<li>
<strong>GUI 渲染线程</strong>:负责渲染浏览器界面 HTML 元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在 Javascript 引擎运行脚本期间,GUI 渲染线程都是处于挂起状态的,也就是说被”冻结”了。</li>
<li>
<strong>JavaScript 引擎线程</strong>:主要负责处理 Javascript 脚本程序。</li>
<li>
<strong>定时器触发线程</strong>:浏览器定时计数器并不是由 JavaScript 引擎计数的, JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此浏览器通过单独线程来计时并触发定时。</li>
<li>
<strong>事件触发线程</strong>:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件包括当前执行的代码块如定时任务、浏览器内核的其他线程如鼠标点击、AJAX 异步请求等。由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。定时块任何和 ajax 请求等这些异步任务,事件触发线程只是在到达定时时间或者是 ajax 请求成功后,把回调函数放到事件队列当中。</li>
<li>
<strong>异步 HTTP 请求线程</strong>:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。在发起了一个异步请求时,http 请求线程则负责去请求服务器,有了响应以后,事件触发线程再把回到函数放到事件队列当中。</li>
</ul>
<h2>2、构建DOM树与CSSOM树</h2>
<p>浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树:</p>
<ul>
<li>
<strong>编码</strong>: 先将HTML的原始字节数据转换为文件指定编码的字符。</li>
<li>
<strong>令牌化</strong>: 然后浏览器会根据HTML规范来将字符串转换成各种令牌(如<code><html>、<body></code>这样的标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义和一组规则)。令牌记录了标签的开始与结束,通过这个特性可以轻松判断一个标签是否为子标签(假设有<code><html></code>与<code><body></code>两个标签,当<code><html></code>标签的令牌还未遇到它的结束令牌<code></html></code>就遇见了<code><body></code>标签令牌,那么<code><body></code>就是<code><html></code>的子标签)。</li>
<li>
<strong>生成对象</strong>: 接下来每个令牌都会被转换成定义其属性和规则的对象(这个对象就是节点对象)</li>
<li>
<strong>构建完毕</strong>: DOM树构建完成,整个对象集合就像是一棵树形结构。可能有人会疑惑为什么DOM是一个树形结构,这是因为标签之间含有复杂的父子关系,树形结构正好可以诠释这个关系(CSSOS同理,层叠样式也含有父子关系。例如: div p {font-size: 18px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。</li>
</ul>
<p>整个DOM树的构建过程其实就是: <strong>字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,</strong><br>下面将通过一个示例HTML代码与配图更形象地解释这个过程。</p>
<pre><code class="html"><html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html></code></pre>
<p><img src="https://segmentfault.com/img/bVbrt5t" alt="DOM树构建过程" title="DOM树构建过程"></p>
<p>当上述HTML代码遇见<link>标签时,浏览器会发送请求获得该标签中标记的CSS文件(使用内联CSS可以省略请求的步骤提高速度,但没有必要为了这点速度而丢失了模块化与可维护性),style.css中的内容如下:</p>
<pre><code class="css">body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }</code></pre>
<p>浏览器获得外部CSS文件的数据后,就会像构建DOM树一样开始构建CSSOM树,这个过程没有什么特别的差别。<br><img src="https://segmentfault.com/img/bVbrt5u" alt="CSSOM树" title="CSSOM树"></p>
<h2>3、构建渲染树</h2>
<p>在构建了DOM树和CSSOM树之后,浏览器只是拥有了两个互相独立的对象集合,DOM树描述了文档的结构与内容,CSSOM树则描述了对文档应用的样式规则,想要渲染出页面,就需要将DOM树与CSSOM树结合在一起,这就是渲染树。<br><img src="https://segmentfault.com/img/bVbrt5y" alt="渲染树" title="渲染树"></p>
<ul>
<li>浏览器会先从DOM树的根节点开始遍历每个可见节点(不可见的节点自然就没必要渲染到页面了,不可见的节点还包括被CSS设置了display: none属性的节点,值得注意的是visibility: hidden属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,所以它会被渲染成一个空框)</li>
<li>对每个可见节点,找到其适配的CSS样式规则并应用。</li>
<li>渲染树构建完成,每个节点都是可见节点并且都含有其内容和对应规则的样式。</li>
</ul>
<h2>4、布局与绘制</h2>
<p>CSS采用了一种叫做盒子模型的思维模型来表示每个节点与其他元素之间的距离,盒子模型包括外边距(Margin),内边距(Padding),边框(Border),内容(Content)。页面中的每个标签其实都是一个个盒子</p>
<p><img src="https://segmentfault.com/img/bVbrt5F" alt="盒子模型" title="盒子模型"><br>布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对的测量值也都会被转换为屏幕内的绝对像素值。</p>
<pre><code class="html"><html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html></code></pre>
<p><img src="https://segmentfault.com/img/bVbrt5G" alt="图片描述" title="图片描述"></p>
<p>当Layout布局事件完成后,浏览器会立即发出Paint Setup与Paint事件,开始将渲染树绘制成像素,绘制所需的时间跟CSS样式的复杂度成正比,绘制完成后,用户就可以看到页面的最终呈现效果了。</p>
<p>我们对一个网页发送请求并获得渲染后的页面可能也就经过了1~2秒,但浏览器其实已经做了上述所讲的非常多的工作,总结一下浏览器关键渲染路径的整个过程:</p>
<ul>
<li>处理HTML标记数据并生成DOM树。</li>
<li>处理CSS标记数据并生成CSSOM树。</li>
<li>将DOM树与CSSOM树合并在一起生成渲染树。</li>
<li>遍历渲染树开始布局,计算每个节点的位置信息。</li>
<li>将每个节点绘制到屏幕。</li>
</ul>
<h2>5、外部资源是如何请求的</h2>
<p>为了直观的观察浏览器加载和渲染的细节,本地用nodejs搭建一个简单的HTTP Server。<br>index.js</p>
<pre><code class="js">const http = require('http');
const fs = require('fs');
const hostname = '127.0.0.1';
const port = 8080;
http.createServer((req, res) => {
if (req.url == '/a.js') {
fs.readFile('a.js', 'utf-8', function (err, data) {
res.writeHead(200, {'Content-Type': 'text/plain'});
setTimeout(function () {
res.write(data);
res.end()
}, 5000)
})
} else if (req.url == '/b.js') {
fs.readFile('b.js', 'utf-8', function (err, data) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write(data);
res.end()
})
} else if (req.url == '/style.css') {
fs.readFile('style.css', 'utf-8', function (err, data) {
res.writeHead(200, {'Content-Type': 'text/css'});
res.write(data);
res.end()
})
} else if (req.url == '/index.html') {
fs.readFile('index.html', 'utf-8', function (err, data) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write(data);
res.end()
})
}
}).listen(port, hostname, () => {
console.log('Server running at ' + hostname + ':' + port);
});</code></pre>
<p>index.html</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
<script src='http://127.0.0.1:8080/a.js'></script>
</head>
<body>
<p id='header'>1111111</p>
<script src='http://127.0.0.1:8080/b.js'></script>
<p>222222</p>
<p>3333333</p>
</body>
</html></code></pre>
<p>style.css</p>
<pre><code class="css">#header{
color: red;
}</code></pre>
<p><code>a.js、b.js</code>暂时为空<br>可以看到,服务端将对a.js的请求延迟5秒返回。Server启动后,在chrome浏览器中打开<a href="https://link.segmentfault.com/?enc=430%2BwyHGHzOU%2BGNKutG%2FUQ%3D%3D.Ulw%2BnYaXsBvrpA511e%2Fv5nk66Nzcj%2FKnJnhXVRn4ETqrGTeq5eNuljlNWZj0USiw" rel="nofollow">http://127.0.0.1:8080/index.html</a><br>我们打开chrome的调试面板<br><img src="https://segmentfault.com/img/bVbrwWV" alt="图片描述" title="图片描述"><br>第一次解析html的时候,外部资源好像是一起请求的,说资源是预解析加载的,就是说style.css和b.js是a.js造成阻塞的时候才发起的请求,图中也是可以解释得通,因为第一次Parse HTML的时候就遇到阻塞,然后预解析就去发起请求,所以看起来是一起请求的。</p>
<h2>6、HTML 是否解析一部分就显示一部分</h2>
<p>我们修改一下html代码</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
</head>
<body>
<p id='header'>1111111</p>
<script src='http://127.0.0.1:8080/a.js'></script>
<script src='http://127.0.0.1:8080/b.js'></script>
<p>222222</p>
<p>3333333</p>
</body>
</html></code></pre>
<p><img src="https://segmentfault.com/img/bVbrw0r" alt="图片描述" title="图片描述"><br>因为a.js的延迟,解析到a.js所在的script标签的时候,a.js还没有下载完成,阻塞并停止解析,之前解析的已经绘制显示出来了。当a.js下载完成并执行完之后继续后面的解析。当然,浏览器不是解析一个标签就绘制显示一次,当遇到阻塞或者比较耗时的操作的时候才会先绘制一部分解析好的。</p>
<h2>7、js文件的位置对HTML解析有什么影响</h2>
<h3>7.1 js文件在头部加载。</h3>
<p>修改index.html:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
<script src='http://127.0.0.1:8080/a.js'></script>
<script src='http://127.0.0.1:8080/b.js'></script>
</head>
<body>
<p id='header'>1111111</p>
<p>222222</p>
<p>3333333</p>
</body>
</html></code></pre>
<p><img src="https://segmentfault.com/img/bVbrxja" alt="图片描述" title="图片描述"><br><strong>因为a.js的阻塞使得解析停止,a.js下载完成之前,页面无法显示任何东西。</strong></p>
<h3>7.2、js文件在中间加载。</h3>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
</head>
<body>
<p id='header'>1111111</p>
<script src='http://127.0.0.1:8080/a.js'></script>
<script src='http://127.0.0.1:8080/b.js'></script>
<p>222222</p>
<p>3333333</p>
</body>
</html>
</code></pre>
<p><img src="https://segmentfault.com/img/bVbrxjb" alt="图片描述" title="图片描述"><br><strong>解析到js文件时出现阻塞。阻塞后面的解析,导致后面的不能很快的显示。</strong></p>
<h3>7.3、js文件在尾部加载。</h3>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
</head>
<body>
<p id='header'>1111111</p>
<p>222222</p>
<p>3333333</p>
<script src='http://127.0.0.1:8080/a.js'></script>
<script src='http://127.0.0.1:8080/b.js'></script>
</body>
</html></code></pre>
<p>解析到a.js部分的时候,页面要显示的东西已经解析完了,a.js不会影响页面的呈现速度。</p>
<p>由上面我们可以总结一下</p>
<ul>
<li>直接引入的 JS 会阻塞页面的渲染(GUI 线程和 JS 线程互斥)</li>
<li>JS 不阻塞资源的加载</li>
<li>JS 顺序执行,阻塞后续 JS 逻辑的执行</li>
</ul>
<p>下面我们来看下异步js</p>
<h3>7.4、async和defer的作用是什么?有什么区别?</h3>
<p>接下来我们对比下 defer 和 async 属性的区别:<br><img src="https://segmentfault.com/img/bVbrw2u" alt="图片描述" title="图片描述"><br>其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。</p>
<ul><li>情况1<code><script src="script.js"></script></code>
</li></ul>
<p>没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。</p>
<ul><li>情况2<script async src="script.js"></script> (异步下载)</li></ul>
<p>async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。</p>
<ul><li>情况3 <script defer src="script.js"></script>(延迟执行)</li></ul>
<p>defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。</p>
<p>defer 与相比普通 script,有两点区别:</p>
<ul>
<li>载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。</li>
<li>在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。</li>
</ul>
<h2>8、css文件的影响</h2>
<p>服务端将<code>style.css</code>的相应也设置延迟。</p>
<pre><code class="js">fs.readFile('style.css', 'utf-8', function (err, data) {
res.writeHead(200, {'Content-Type': 'text/css'});
setTimeout(function () {
res.write(data);
res.end()
}, 5000)
})</code></pre>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
</head>
<body>
<p id='header'>1111111</p>
<p>222222</p>
<p>3333333</p>
<script src='http://127.0.0.1:8080/a.js' async></script>
<script src='http://127.0.0.1:8080/b.js' async></script>
</body>
</html>
</code></pre>
<p>可以看出来,css文件<strong>不会阻塞HTML解析,但是会阻塞渲染</strong>,导致css文件未下载完成之前已经解析好html也无法先显示出来。</p>
<p>我们把css调整到尾部</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>浏览器渲染</title>
</head>
<body>
<p id='header'>1111111</p>
<p>222222</p>
<p>3333333</p>
<link rel="stylesheet" href="http://127.0.0.1:8080/style.css">
<script src='http://127.0.0.1:8080/a.js' async></script>
<script src='http://127.0.0.1:8080/b.js' async></script>
</body>
</html></code></pre>
<p>这是页面可以渲染了,但是没有样式。直到css加载完成</p>
<p>以上我们可以简单总结。</p>
<ul>
<li>CSS 放在 head 中会阻塞页面的渲染(页面的渲染会等到 css 加载完成)</li>
<li>CSS 阻塞 JS 的执行 (因为 GUI 线程和 JS 线程是互斥的,因为有可能 JS 会操作 CSS)</li>
<li>CSS 不阻塞外部脚本的加载(不阻塞 JS 的加载,但阻塞 JS 的执行,因为浏览器都会有预先扫描器)</li>
</ul>
<p>参考<br><a href="https://link.segmentfault.com/?enc=7K%2FnvdO4gWUs1ruOzWERjg%3D%3D.SbCVhQuw5bVQt3w6LChw5xtjNe3BHjbmdjXLgrHjpx4A9KbjlixHmxZmENJgRuDg" rel="nofollow">浏览器渲染过程与性能优化</a><br><a href="https://segmentfault.com/a/1190000007766425">聊聊浏览器的渲染机制</a><br><a href="https://segmentfault.com/a/1190000018811208">你不知道的浏览器页面渲染机制</a></p>
跨域方案总结
https://segmentfault.com/a/1190000018864955
2019-04-14T21:18:55+08:00
2019-04-14T21:18:55+08:00
Alan
https://segmentfault.com/u/xc_xiang
8
<blockquote>平时在开发中总是会遇到各种跨域问题,一直没有很好地了解其中的原理,以及其各种实现方案。今天在这好好总结一下。</blockquote>
<p>本文完整的源代码请猛戳<a href="https://link.segmentfault.com/?enc=Sso0Mgen%2BrS6rXDayGFc9Q%3D%3D.pRTuMUyXC%2BNZfcDO2q1%2BgLkGPtrC3JPGZ7CPH5h4JbBlryczlw819GY4SomKNC7fFieHAzDIzQoHAQaGh4hZlJ0%2FdINIhaCpl1m8n2T55Qw%3D" rel="nofollow">github</a>博客,建议大家动手敲敲代码。</p>
<h2>1、什么是跨域?为什么会有跨域?</h2>
<p>一般来说,当一个请求url的协议、域名、端口三者之间任意一个与当前页面地址不同即为跨域。<br>之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。</p>
<p><strong>为什么会有同源策略呢?</strong><br>同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。</p>
<p>设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?</p>
<p>很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。</p>
<p>由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。</p>
<p><strong>同源策略限制内容有:</strong></p>
<ul>
<li>Cookie、LocalStorage、IndexedDB 等存储性内容</li>
<li>DOM 节点</li>
<li>AJAX 请求发送后,结果被浏览器拦截了</li>
</ul>
<p>下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:</p>
<ul>
<li>
<code><script src="..."></script></code>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。</li>
<li>
<code><link rel="stylesheet" href="..."></code>标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的<code>Content-Type</code>消息头。不同浏览器有不同的限制: <code>IE, Firefox, Chrome, Safari</code> 和 <code>Opera</code>。</li>
<li>
<code><img></code>嵌入图片。支持的图片格式包括<code>PNG,JPEG,GIF,BMP,SVG</code>
</li>
<li>
<code><video></code> 和 <code><audio></code>嵌入多媒体资源。</li>
<li>
<code><object></code>, <code><embed></code> 和 <code><applet></code>的插件。</li>
<li>
<code>@font-face</code>引入的字体。一些浏览器允许跨域字体<code>( cross-origin fonts)</code>,一些需要同源字体<code>(same-origin fonts)</code>。</li>
<li>
<code><frame></code>和<code><iframe></code>载入的任何资源。站点可以使用<code>X-Frame-Options</code>消息头来阻止这种形式的跨域交互。</li>
</ul>
<p><strong>常见的跨域场景</strong></p>
<pre><code class="js">URL 说明 是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js
http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许
http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许
http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许
http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js
http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许</code></pre>
<blockquote>注意:关于跨域,有两个误区:<br>1、动态请求就会有跨域的问题(错)。跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/ java等其它环境<br>2、跨域就是请求发不出去了(错)。跨域请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了</blockquote>
<h2>2、跨域的解决方案</h2>
<h3>2.1、jsonp</h3>
<p><code>jsonp</code>的跨域原理是利用<code>script</code>标签不受跨域限制而形成的一种方案。<br>下面我们来简单看一下代码实现</p>
<pre><code class="html"><!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为onBack
script.src = 'http://127.0.0.1:3000/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}
</script>
</body>
</html></code></pre>
<p>node</p>
<pre><code class="js">var qs = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
console.log(req);
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('3000');
console.log('Server is running at port 3000...');</code></pre>
<p>我们可以看到返回的结果:<br><img src="https://segmentfault.com/img/bVbrjjR" alt="图片描述" title="图片描述"></p>
<ul>
<li>优点:兼容性好(兼容低版本IE)</li>
<li>缺点:1.JSONP只支持GET请求; 2.XMLHttpRequest相对于JSONP有着更好的错误处理机制</li>
</ul>
<h3>2.2、postMessage</h3>
<p>postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一。<br>语法:<code>otherWindow.postMessage(message, targetOrigin, [transfer])</code>;</p>
<ul>
<li>
<code>otherWindow</code>:指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口;</li>
<li>
<p><code>message</code> 属性是要发送的消息,类型为 String、Object (IE8、9 不支持);</p>
<ul>
<li>
<code>data</code> 属性为 window.postMessage 的第一个参数;</li>
<li>
<code>origin</code> 属性表示调用window.postMessage() 方法时调用页面的当前状态;</li>
<li>
<code>source</code> 属性记录调用 window.postMessage() 方法的窗口信息;</li>
</ul>
</li>
<li>
<code>targetOrigin</code>:属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。</li>
<li>
<code>transfer</code>:是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。</li>
</ul>
<p>看一下简单的demo</p>
<pre><code class="html"><!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>AAAAAAAAAAAA</h1>
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
<script>
function load(params){
let iframe = document.getElementById('frame');
iframe.onload = function() {
const data = {
name: 'aym'
};
//获取iframe中的窗口,给iframe里嵌入的window发消息
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://localhost:4000');
};
// 接收b.html回过来的消息
window.onmessage = function(e){
console.log(e.data)
}
}
</script>
</body>
</html>
</code></pre>
<pre><code class="html"><!-- b.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>BBBBBBBBB</h1>
<script>
window.addEventListener('message', function(e) {
console.log('data from domain1 ---> ' + e.data);
let data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://127.0.0.1:8080');
}
}, false);
</script>
</body>
</html></code></pre>
<p><img src="https://segmentfault.com/img/bVbrjn1" alt="图片描述" title="图片描述"></p>
<h3>2.3、websocket</h3>
<p>WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。<br>原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。</p>
<pre><code class="html"><!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div>user input:<input type="text"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.dev.js"></script>
<script>
var socket = io('http://127.0.0.1:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
</body>
</html></code></pre>
<p>node服务端文件</p>
<pre><code class="js">var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
</code></pre>
<p><img src="https://segmentfault.com/img/bVbrjqk" alt="图片描述" title="图片描述"><br><img src="https://segmentfault.com/img/bVbrjqw" alt="图片描述" title="图片描述"></p>
<h3>2.4、Node中间件代理</h3>
<p>实现原理:<strong>同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。</strong></p>
<p>主要访问路径</p>
<ul>
<li>客户端发出请求</li>
<li>代理服务接受客户端请求 。</li>
<li>大理服务将请求 转发给应用服务器。</li>
<li>应用服务器接收到请求代理服务器求情 ,响应数据。</li>
<li>代理服务器将响应数据转发给客户端。</li>
</ul>
<p>实现代码:<br>前端代码示例:</p>
<pre><code class="html"><!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>1111</h1>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://127.0.0.1:3000/login?user=admin&password=123',
success: function(result) {
console.log(result)
},
error: function(msg) {
console.log(msg)
}
})
</script>
</body>
</html></code></pre>
<p>代理服务器</p>
<pre><code class="js">var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
var options = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1d',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}
app.use(express.static('public', options))
app.use('/', proxy({
// 代理跨域目标接口
target: 'http://127.0.0.1:4000',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改响应信息中的cookie域名
cookieDomainRewrite: '127.0.0.1' // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');</code></pre>
<p>应用服务器</p>
<pre><code class="js">// 服务器
const http = require("http");
const server = http.createServer();
const qs = require("querystring");
server.on("request", function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
console.log(req.url, params);
// 向前台写 cookie
res.writeHead(200, {
"Set-Cookie": "l=a123456;Path=/;Domain=127.0.0.1;HttpOnly" // HttpOnly:脚本无法读取
});
res.write(JSON.stringify({ data: 'I LOVE YOU', ...params }));
res.end();
});
server.listen("4000");
console.log('listen 4000...')</code></pre>
<p>最终效果<br><img src="https://segmentfault.com/img/bVbrjx8" alt="图片描述" title="图片描述"></p>
<h3>2.5、nginx反向代理</h3>
<p><strong>跨域原理</strong>: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。</p>
<p>实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。</p>
<p>nginx具体配置:</p>
<pre><code>#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}</code></pre>
<p>Nodejs后台示例:</p>
<pre><code class="js">var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');</code></pre>
<p>前端代码示例:</p>
<pre><code class="js">var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();</code></pre>
<h3>2.6、CORS</h3>
<p>普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。<br>虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为<strong>简单请求</strong>和<strong>复杂请求</strong>。</p>
<p><strong>简单请求</strong><br>只要同时满足以下两大条件,就属于简单请求</p>
<ul>
<li>1:使用下列方法之一:<code>GET、HEAD、POST</code>
</li>
<li>2:Content-Type 的值仅限于下列三者之一:<code>text/plain</code>、<code>multipart/form-data</code>、<code>application/x-www-form-urlencoded</code>
</li>
</ul>
<p><strong>复杂请求</strong><br>凡是不同时满足上面两个条件,就属于复杂请求。</p>
<p>复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。</p>
<p>我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:</p>
<pre><code class="js">// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
</code></pre>
<p>接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段</p>
<pre><code class="js">// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()</code></pre>
<pre><code class="js">//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
</code></pre>
<pre><code class="js">//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
</code></pre>
<p><img src="https://segmentfault.com/img/bVbrjMH" alt="图片描述" title="图片描述"></p>
<h3>2.7、location name +iframe</h3>
<p><strong>原理</strong>:window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在。<br>下面<code>a.html</code>和<code>b.html</code>是同域的,都是<code>http://localhost:3000</code>;而<code>c.html</code>是<code>http://localhost:4000</code></p>
<pre><code class="js">// a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script></code></pre>
<p>b.html为中间代理页,与a.html同域,内容为空。<br>c页面</p>
<pre><code class="js"> // c.html(http://localhost:4000/c.html)
<script>
window.name = '我不爱你'
</script></code></pre>
<p>总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。</p>
<h3>2.8、document. hash + iframe</h3>
<p><strong>实现原理</strong>: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。</p>
<p><strong>具体实现步骤</strong>:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。<br>同样的,a.html和b.html是同域的,都是<a href="https://link.segmentfault.com/?enc=mPGUGYyXNNVPv3hmRdjW5g%3D%3D.KucNKoUU9beHP%2BNa7mIyHcIPVBcM5SXqbACWVoL5EUk%3D" rel="nofollow">http://localhost</a>:3000;而c.html是<a href="https://link.segmentfault.com/?enc=oGVjef1dyE7U6qQ%2B2137fg%3D%3D.FKt8Peo3Pi8tEW9as9SExdXlqYoBeR0qy79cEquvDaE%3D" rel="nofollow">http://localhost</a>:4000</p>
<pre><code class="js"> // a.html
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
window.onhashchange = function () { //检测hash的变化
console.log(location.hash);
}
</script></code></pre>
<pre><code class="js"> // b.html
<script>
window.parent.parent.location.hash = location.hash
//b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script></code></pre>
<pre><code class="js"> // c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#idontloveyou';
document.body.appendChild(iframe);</code></pre>
<h3>2.9、 document.domain + iframe</h3>
<p><strong>实现原理</strong>:两个页面都通过js强制设置<code>document.domain</code>为基础主域,就实现了同域。</p>
<p>该方式只能用于二级域名相同的情况下,比如 <code>a.test.com</code> 和 <code>b.test.com</code> 适用于该方式。 只需要给页面添加 <code>document.domain ='test.com'</code> 表示二级域名都相同就可以实现跨域。</p>
<p>我们看个例子:页面<code>a.zf1.cn:3000/a.html</code>获取页面<code>b.zf1.cn:3000/b.html</code>中a的值</p>
<pre><code class="js">// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body></code></pre>
<pre><code class="js">// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body></code></pre>
<h2>3、总结</h2>
<ol>
<li>日常工作中,用得比较多的跨域方案是cors和nginx反向代理</li>
<li>CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案</li>
<li>不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。</li>
<li>SONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。</li>
</ol>
<blockquote>后续更多文章将在我的<a href="https://link.segmentfault.com/?enc=unR1xtSrG2k5ddYuIKa%2Fxw%3D%3D.cYXo85LtVcwY7jDK1UQBdQDFsW8jig4%2FhEII7NzIOc1lHITJvZTTP9pWdVWo475N" rel="nofollow">github</a>第一时间发布,欢迎关注。</blockquote>
<p><strong>参考</strong><br><a href="https://link.segmentfault.com/?enc=oV1Y4UnTR2VBwTnggAHACg%3D%3D.xr7byX7VjZupp3XmPAgheGRs561k7g%2FNSLVr%2FViPxQKDX636xqbdMZsFpFu4x8XO7UncmPgOPUlgtv%2FGbGqHjQ%3D%3D" rel="nofollow">浏览器同源政策及其规避方法</a><br><a href="https://link.segmentfault.com/?enc=9lupP2zYIKryPfa7BLRuzg%3D%3D.16qrN1EQWaaH6W74m8Ks8gA1fTaH%2BXUMxdJe6rhsjdyxMrdYsxPSz63eZEQpzGs%2BwO07781Q8ExkRRn%2BAQ1YhA%3D%3D" rel="nofollow">跨域资源共享 CORS 详解</a><br><a href="https://segmentfault.com/a/1190000011145364#articleHeader0">前端常见跨域解决方案(全)</a></p>
前端应该知道的http
https://segmentfault.com/a/1190000018841101
2019-04-12T08:55:49+08:00
2019-04-12T08:55:49+08:00
Alan
https://segmentfault.com/u/xc_xiang
13
<blockquote>作为互联网通信协议的一员老将,HTTP 协议走到今天已经经历了三次版本的变动,现在最新的版本是 HTTP2.0,相信大家早已耳熟能详。今天就给大家好好介绍一下 HTTP 的前世今生。</blockquote>
<h2>1、http的历史简介</h2>
<p>先简单的介绍一下,后面再具体详解</p>
<h3>1.1、HTTP/0.9</h3>
<p>HTTP 的最早版本诞生在 1991 年,这个最早版本和现在比起来极其简单,没有 HTTP 头,没有状态码,甚至版本号也没有,后来它的版本号才被定为 0.9 来和其他版本的 HTTP 区分。HTTP/0.9 只支持一种方法—— Get,请求只有一行。</p>
<pre><code class="js">GET /hello.html</code></pre>
<p>响应也是非常简单的,只包含 html 文档本身。</p>
<pre><code class="js"><HTML>
Hello world
</HTML></code></pre>
<p>当 TCP 建立连接之后,服务器向客户端返回 HTML 格式的字符串。发送完毕后,就关闭 TCP 连接。由于没有状态码和错误代码,如果服务器处理的时候发生错误,只会传回一个特殊的包含问题描述信息的 HTML 文件。这就是最早的 HTTP/0.9 版本。</p>
<h3>1.2、HTTP/1.0</h3>
<p>1996 年,HTTP/1.0 版本发布,大大丰富了 HTTP 的传输内容,除了文字,还可以发送图片、视频等,这为互联网的发展奠定了基础。相比 HTTP/0.9,HTTP/1.0 主要有如下特性:</p>
<ul>
<li>请求与响应支持 HTTP 头,增加了状态码,响应对象的一开始是一个响应状态行</li>
<li>协议版本信息需要随着请求一起发送,支持 HEAD,POST 方法</li>
</ul>
<p>支持传输 HTML 文件以外其他类型的内容 一个典型的 HTTP/1.0 的请求像这样:</p>
<pre><code class="js">GET /hello.html HTTP/1.0
User-Agent:NCSA_Mosaic/2.0(Windows3.1)
200 OK
Date: Tue, 15 Nov 1996 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/html
<HTML>
一个包含图片的页面
<IMGSRC="/smile.gif">
</HTML>
</code></pre>
<h2>1.3、HTTP/1.1</h2>
<p>在 HTTP/1.0 发布几个月后,HTTP/1.1 就发布了。HTTP/1.1 更多的是作为对 HTTP/1.0 的完善,在 HTTP1.1 中,主要具有如下改进:</p>
<ul>
<li>可以复用连接</li>
<li>增加 pipeline</li>
<li>chunked 编码传输</li>
<li>引入更多缓存控制机制</li>
<li>引入内容协商机制</li>
<li>请求消息和响应消息都支持 Host 头域</li>
<li>新增了 OPTIONS,PUT, DELETE, TRACE, CONNECT 方法</li>
</ul>
<h3>1.4、 HTTPS</h3>
<p>HTTPS 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。</p>
<p>HTTPS 协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。 HTTPS 和 HTTP 的区别主要如下:</p>
<ul>
<li>HTTPS 协议使用 ca 申请证书,由于免费证书较少,需要一定费用。</li>
<li>HTTP 是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。</li>
<li>HTTP 和 HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。</li>
</ul>
<h3>1.5、SPDY</h3>
<p>在 2010 年到 2015 年,谷歌通过实践一个实验性的 SPDY 协议,证明了一个在客户端和服务器端交换数据的另类方式。其收集了浏览器和服务器端的开发者的焦点问题,明确了响应数量的增加和解决复杂的数据传输。在启动 SPDY 这个项目时预设的目标是:</p>
<ul>
<li>页面加载时间 (PLT) 减少 50%。</li>
<li>无需网站作者修改任何内容。</li>
<li>将部署复杂性降至最低,无需变更网络基础设施。</li>
<li>与开源社区合作开发这个新协议。</li>
<li>收集真实性能数据,验证这个实验性协议是否有效。</li>
</ul>
<p>为了达到降低目标,减少页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。</p>
<h3>1.6、 HTTP/2.0</h3>
<p>时间来到 2015 年,HTTP/2.0 问世。先来介绍一下 HTTP/2.0 的特点吧:</p>
<ul>
<li>使用二进制分帧层</li>
<li>多路复用</li>
<li>数据流优先级</li>
<li>服务端推送</li>
<li>头部压缩</li>
</ul>
<h2>2、http原理详解</h2>
<p>HTTP协议是构建在TCP/IP协议之上的,是TCP/IP协议的一个子集,所以要理解HTTP协议,有必要先了解下TCP/IP协议相关的知识。</p>
<h3>2.1 TCP/IP协议</h3>
<p>TCP/IP协议族是由一个四层协议组成的系统,这四层分别为:应用层、传输层、网络层和数据链路层<br><img src="/img/bVbrdsC?w=1064&h=998" alt="图片描述" title="图片描述"></p>
<p>分层的好处是把各个相对独立的功能解耦,层与层之间通过规定好的接口来通信。如果以后需要修改或者重写某一个层的实现,只要接口保持不变也不会影响到其他层的功能。接下来,我们将会介绍各个层的主要作用。<br><strong>1) 应用层</strong><br>应用层一般是我们编写的应用程序,其决定了向用户提供的应用服务。应用层可以通过系统调用与传输层进行通信。<br>处于应用层的协议非常多,比如:FTP(File Transfer Protocol,文件传输协议)、DNS(Domain Name System,域名系统)和我们本章讨论的HTTP(HyperText Transfer Protocol,超文本传输协议)等。<br><strong>2) 传输层</strong><br>传输层通过系统调用向应用层提供处于网络连接中的两台计算机之间的数据传输功能。<br>在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和UDP(User Data Protocol,用户数据报协议)。<br><strong>3) 网络层</strong><br>网络层用来处理在网络上流动的数据包,数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(传输路线)到达对方计算机,并把数据包传输给对方。IP协议<br><strong>4) 链路层</strong><br>链路层用来处理连接网络的硬件部分,包括控制操作系统、硬件设备驱动、NIC(Network Interface Card,网络适配器)以及光纤等物理可见部分。硬件上的范畴均在链路层的作用范围之内。</p>
<p><strong>数据包封装</strong><br>上层协议数据是如何转变为下层协议数据的呢?这是通过封装(encapsulate)来实现的。应用程序数据在发送到物理网络之前,会沿着协议栈从上往下传递。每层协议都将在上层协议数据的基础上加上自己的头部信息(链路层还会加上尾部信息),以为实现该层功能提供必要的信息.<br><img src="/img/bVbrdsH?w=1190&h=592" alt="图片描述" title="图片描述"><br>发送端发送数据时,数据会从上层传输到下层,且每经过一层都会被打上该层的头部信息。而接收端接收数据时,数据会从下层传输到上层,传输前会把下层的头部信息删除.</p>
<p>由于下层协议的头部信息对上层协议是没有实际的用途,所以在下层协议传输数据给上层协议的时候会把该层的头部信息去掉,这个封装过程对于上层协议来说是完全透明的。这样做的好处是,应用层只需要关心应用服务的实现,而不用管底层的实现。</p>
<p><strong>TCP三次握手</strong><br>从上面的介绍可知,传输层协议主要有两个:TCP协议和UDP协议。TCP协议相对于UDP协议的特点是:TCP协议提供面向连接、字节流和可靠的传输。</p>
<p><img src="/img/bVbrdsK?w=1036&h=690" alt="图片描述" title="图片描述"></p>
<ul>
<li>第一次握手:客户端发送带有SYN标志的连接请求报文段,然后进入SYN_SEND状态,等待服务端的确认。</li>
<li>第二次握手:服务端接收到客户端的SYN报文段后,需要发送ACK信息对这个SYN报文段进行确认。同时,还要发送自己的SYN请求信息。服务端会将上述的信息放到一个报文段(SYN+ACK报文段)中,一并发送给客户端,此时服务端将会进入SYN_RECV状态。</li>
<li>第三次握手:客户端接收到服务端的SYN+ACK报文段后,会想服务端发送ACK确认报文段,这个报文段发送完毕后,客户端和服务端都进入ESTABLISHED状态,完成TCP三次握手。</li>
</ul>
<p>当三次握手完成后,TCP协议会为连接双方维持连接状态。为了保证数据传输成功,接收端在接收到数据包后必须发送ACK报文作为确认。如果在指定的时间内(这个时间称为重新发送超时时间),发送端没有接收到接收端的ACK报文,那么就会重发超时的数据。</p>
<h3>2.2、 DNS 域名解析</h3>
<p>当你在浏览器的地址栏输入 <a href="https://link.segmentfault.com/?enc=M2olVK0HvvqJdifofpdthw%3D%3D.quV9y4tDxCpx9Whgn1dKIx7UBQuXUeELDD245NsBoCc%3D" rel="nofollow">https://juejin.im</a> 后会发生什么,大家在心中肯定是有一个大概的,这里我将 DNS 域名解析 这个步骤详细的讲一遍。在讲概念之前我先放上一张经典的图文供大家思考一分钟。<br><img src="/img/bVbrdsO?w=1248&h=560" alt="图片描述" title="图片描述"><br><strong>查找域名对应的 IP 地址的具体过程</strong></p>
<ol>
<li>浏览器搜索自己的 DNS 缓存(浏览器维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;</li>
<li>搜索操作系统中的 DNS 缓存;如果没有命中,进入下一步;</li>
<li>搜索操作系统的 hosts 文件( Windows 环境下,维护一张域名与 IP 地址的对应表);如果没有命中,进入下一步;</li>
<li>
<p>列表项目</p>
<ul>
<li>操作系统将域名发送至 LDNS (本地区域名服务器),LDNS 查询自己的 DNS 缓存(一般命中率在 80% 左右),查找成功则返回结果,失败则发起一个迭代 DNS 解析请求:</li>
<li>LDNS向 Root Name Server(根域名服务器,如com、net、im 等的顶级域名服务器的地址)发起请求,此处,Root Name Server 返回 im 域的顶级域名服务器的地址;</li>
<li>LDNS 向 im 域的顶级域名服务器发起请求,返回 juejin.im 域名服务器地址;</li>
<li>LDNS 向 juejin.im 域名服务器发起请求,得到 juejin.im 的 IP 地址;</li>
<li>LDNS 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来;操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来。</li>
</ul>
</li>
</ol>
<p>http工作的简单过程</p>
<ul>
<li>地址解析: 这一步比较重要的是上面的DNS解析</li>
<li>封装HTTP请求数据包: 把以上部分结合本机自己的信息,封装成一个HTTP请求数据包</li>
<li>封装成TCP包,建立TCP连接(TCP的三次握手)</li>
<li>客户机发送请求命令</li>
<li>服务器响应</li>
<li>服务器关闭TCP连接</li>
</ul>
<h3>2.3、http请求方法</h3>
<p>一些常见的http请求方法。</p>
<ul>
<li>GET: 用于获取数据</li>
<li>POST: 用于将实体提交到指定的资源,通常导致状态或服务器上的副作用的更改</li>
<li>HEAD: 与GET请求的响应相同的响应,但没有响应体</li>
<li>PUT: 用于创建或更新指定资源</li>
<li>DELETE: 删除指定的资源</li>
</ul>
<p>关于get与post的一些区别。可以看我的另一篇文章<a href="https://segmentfault.com/a/1190000018799171">面试经典之http中get与post的区别</a></p>
<h3>2.4、 http缓存</h3>
<p>http很重要的一点还有他的缓存机制。关于这部分的内容可以看一下我之前的文章<a href="https://segmentfault.com/a/1190000018717463">浏览器缓存看这一篇就够了</a>。这里就不在赘述了。</p>
<h3>2.5、状态码</h3>
<p>这里主要讲一些常用的状态码</p>
<p><strong>1、 301 永久转移</strong><br>当你想换域名的时候,就可以使用301,如之前的域名叫www.renfed.com,后来换了一个新域名fed.renren.com,希望用户访问老域名的时候能够自动跳转到新的域名,那么就可以使用nginx返回301:</p>
<pre><code class="js">server {
listen 80;
server_name www.renfed.com;
root /home/fed/wordpress;
return 301 https://fed.renren.com$request_uri;
}</code></pre>
<p>浏览器收到301之后,就会自动跳转了。搜索引擎在爬的时候如果发现是301,在若干天之后它会把之前收录的网页的域名给换了。</p>
<p>还有一个场景,如果希望访问http的时候自动跳转到https也是可以用301,因为如果直接在浏览器地址栏输入域名然后按回车,前面没有带https,那么是默认的http协议,这个时候我们希望用户能够访问安全的https的,不要访问http的,所以要做一个重定向,也可以使用301,如:</p>
<pre><code class="js">server {
listen 80;
server_name fed.renren.com;
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
}
</code></pre>
<p><strong>2、302 Found 资源暂时转移</strong><br>很多短链接跳转长链接就是使用的302,如下图所示:<br><img src="/img/bVbrdtY?w=2780&h=1070" alt="图片描述" title="图片描述"><br><strong>3、304 Not Modified 没有修改</strong><br>这个主要在上面的缓存哪里出现的比较多。如果服务器没有修改。就会使用浏览器的缓存。</p>
<p><img src="/img/bVbrdt2?w=1276&h=594" alt="图片描述" title="图片描述"><br><strong>4、400 Bad Request 请求无效</strong><br>当必要参数缺失、参数格式不对时,后端通常会返回400,如下图所示:<br><img src="/img/bVbrdt3?w=1282&h=304" alt="图片描述" title="图片描述"></p>
<p><strong>5、403 Forbidden 拒绝服务</strong><br>服务能够理解你的请求,包括传参正确,但是拒绝提供服务。例如,服务允许直接访问静态文件,但是不允许访问某个目录:<br><img src="/img/bVbrdt7?w=1282&h=408" alt="图片描述" title="图片描述"><br>否则,别人对你服务器上的文件就一览无遗了。<br>403和401的区别在于,401是没有认证,没有登陆验证之类的错误。</p>
<p><strong>6、500 内部服务器错误</strong><br>如业务代码出现了异常没有捕获,被tomcat捕获了,就会返回500错误:<br><img src="/img/bVbrdue?w=1266&h=152" alt="图片描述" title="图片描述"><br>如:数据库字段长度限制为30个字符,如果没有判断直接插入一条31个字符的记录,就会导致数据库抛异常,如果异常没有捕获处理,就直接返回500。</p>
<p>当服务彻底挂了,连返回都没有的时候,那么就是502了。</p>
<p><strong>7、502 Bad Gateway 网关错误</strong><br><img src="/img/bVbrduq?w=1294&h=402" alt="图片描述" title="图片描述"><br>这种情况是因为nginx收到请求,但是请求没有打过去,可能是因为业务服务挂了,或者是打过去的端口号写错了</p>
<p><strong>8、504 Gateway Timeout 网关超时</strong><br>通常是因为服务处理请求太久,导致超时,如PHP服务默认的请求响应最长处理时间为30s,如果超过30s,将会挂掉,返回504,如下图所示:<br><img src="/img/bVbrduw?w=1308&h=396" alt="图片描述" title="图片描述"></p>
<h3>2.6、HTTP的基本优化</h3>
<p>影响一个HTTP网络请求的因素主要有两个:<strong>带宽</strong>和<strong>延迟</strong>。</p>
<ul>
<li>带宽<br>如果说我们还停留在拨号上网的阶段,带宽可能会成为一个比较严重影响请求的问题,但是现在网络基础建设已经使得带宽得到极大的提升,我们不再会担心由带宽而影响网速,那么就只剩下延迟了。</li>
<li>延迟<br>1、浏览器阻塞(HOL blocking):浏览器会因为一些原因阻塞请求。浏览器对于同一个域名,同时只能有 4 个连接(这个根据浏览器内核不同可能会有所差异),超过浏览器最大连接数限制,后续请求就会被阻塞。<br>2、DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用DNS缓存结果来达到减少这个时间的目的。<br>3、建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大</li>
</ul>
<p>http的发展也就是在不断地优化这些方向上的问题。</p>
<h2>3、http1.1</h2>
<p>HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在:</p>
<ul>
<li>
<strong>缓存处理</strong>,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。</li>
<li>
<strong>带宽优化及网络连接的使用</strong>,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。</li>
<li>
<strong>错误通知的管理</strong>,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。</li>
<li>
<strong>Host头处理</strong>,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。</li>
<li>
<strong>长连接</strong>,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。</li>
</ul>
<p>虽然 HTTP/1.1 已经优化了很多点,作为一个目前使用最广泛的协议版本,已经能够满足很多网络需求,但是随着网页变得越来越复杂,甚至演变成为独立的应用,HTTP/1.1 逐渐暴露出了一些问题:</p>
<ul>
<li>在传输数据时,每次都要重新建立连接,对移动端特别不友好</li>
<li>传输内容是明文,不够安全</li>
<li>header 内容过大,每次请求 header 变化不大,造成浪费</li>
<li>keep-alive 给服务端带来性能压力 为了解决这些问题,HTTPS 和 SPDY 应运而生。</li>
</ul>
<h2>4、HTTPS</h2>
<p>HTTPS 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。<br><img src="/img/bVbrdw7?w=635&h=239" alt="图片描述" title="图片描述"></p>
<ul>
<li>HTTPS协议需要到CA申请证书,一般免费证书很少,需要交费。</li>
<li>HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。</li>
<li>HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。</li>
<li>HTTPS可以有效的防止运营商劫持,解决了防劫持的一个大问题。</li>
</ul>
<h2>5、SPDY:HTTP1.x的优化</h2>
<p>2012年google如一声惊雷提出了SPDY的方案,优化了HTTP1.X的请求延迟,解决了HTTP1.X的安全性,具体如下:</p>
<ul>
<li>
<strong>降低延迟</strong>,针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。</li>
<li>
<strong>请求优先级</strong>(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。</li>
<li>
<strong>header压缩</strong>。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。</li>
<li>
<strong>基于HTTPS的加密协议传输</strong>,大大提高了传输数据的可靠性。</li>
<li>
<strong>服务端推送</strong>(server push),采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。</li>
</ul>
<p>SPDY构成图:</p>
<p><img src="/img/bVbrdxq?w=351&h=291" alt="图片描述" title="图片描述"><br>SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。</p>
<h2>6、HTTP2.0</h2>
<p>HTTP2.0可以说是SPDY的升级版(其实原本也是基于SPDY设计的),但是,HTTP2.0 跟 SPDY 仍有不同的地方,如下:<br>HTTP2.0和SPDY的区别:</p>
<ul>
<li>HTTP2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS</li>
<li>HTTP2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE</li>
</ul>
<p>HTTP/2 新特性</p>
<h3>6.1、二进制传输</h3>
<p>HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。</p>
<p>接下来我们介绍几个重要的概念:</p>
<ul>
<li>流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2…N);</li>
<li>消息:是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。</li>
<li>帧:HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流,承载着特定类型的数据,如 HTTP 首部、负荷,等等</li>
</ul>
<p><img src="/img/bVbrdyn?w=595&h=487" alt="图片描述" title="图片描述"></p>
<p>HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。</p>
<h3>6.2、多路复用</h3>
<p>在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。</p>
<p>在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2中:</p>
<ul>
<li>同域名下所有通信都在单个连接上完成。</li>
<li>单个连接可以承载任意数量的双向数据流。</li>
<li>数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。</li>
</ul>
<p>这一特性,使性能有了极大提升:</p>
<ul>
<li>同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,消除了因多个 TCP 连接而带来的延时和内存消耗。</li>
<li>并行交错地发送多个请求,请求之间互不影响。</li>
<li>并行交错地发送多个响应,响应之间互不干扰。</li>
<li>在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。</li>
</ul>
<p><img src="/img/bVbrdzf?w=636&h=342" alt="图片描述" title="图片描述"><br>如上图所示,多路复用的技术可以只通过一个 TCP 连接就可以传输所有的请求数据。</p>
<h3>6.3、Header 压缩</h3>
<p>在 HTTP/1 中,我们使用文本的形式传输 header,在 header 携带 cookie 的情况下,可能每次都需要重复传输几百到几千的字节。</p>
<p>为了减少这块的资源消耗并提升性能, HTTP/2对这些首部采取了压缩策略:</p>
<ul>
<li>HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;</li>
<li>首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;</li>
<li>每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值</li>
</ul>
<p>例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销<br><img src="/img/bVbrdzq?w=589&h=441" alt="图片描述" title="图片描述"></p>
<h3>6.4、服务端推送(Server Push)</h3>
<p>Server Push即服务端能通过push的方式将客户端需要的内容预先推送过去,也叫“cache push”。</p>
<p>可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用 prefetch。</p>
<p>例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。<br><img src="/img/bVbrdzS?w=665&h=280" alt="图片描述" title="图片描述"></p>
<p>服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。</p>
<blockquote>后续更多文章将在我的<a href="https://link.segmentfault.com/?enc=C0O6wNaZUGJdFsd3FM5WTw%3D%3D.yQiLeHe5TugUEha2jeZaK%2FcCa8w6Un0HOOxMFL1XcBXhU8j%2BRgq96wOBWyusI0WT" rel="nofollow">github</a>第一时间发布,欢迎关注。</blockquote>
<p><strong>参考</strong></p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=8os7MUO0tvvu4D3GeHd3tA%3D%3D.CnBWGfqGfUx8fllUfZBPU9CwoUdagkkIPGRY5T1UA8Jpmq9zF8OqeaEJmlOJQ6hD4uhjQ9H85S07W%2BzrQmiQ9Q%3D%3D" rel="nofollow">HTTP协议详解</a></li>
<li><a href="https://link.segmentfault.com/?enc=BTuCdPVE0nJZqBjxZfEUhw%3D%3D.zZxFDT2txhzxXHGs0Feg5%2FzVGsFOzrMP1R%2BZtOBY94wmrnDSyhjHjz3tjHeqA8gbfRTkRi0OhUpXK7wF%2BkOgOA%3D%3D" rel="nofollow">前端词典】进阶必备的网络基础</a></li>
<li><a href="https://link.segmentfault.com/?enc=DCQXhqPP6%2FB60Ni7gHLomQ%3D%3D.D6odwsiudF%2F8VyXNjXRy%2B54qrNGejT3CS0r79ciKD3lWIky%2FsC4ilgGjGFHDhoVZ" rel="nofollow">我知道的HTTP请求</a></li>
<li><a href="https://link.segmentfault.com/?enc=S1lNQtubLmMpiqBUdAmBcQ%3D%3D.ZbIFNgf8p5ago42LJ3JewjeCvMwQJLdTyp%2FnS8cqJm7TvYvvyWLakj%2Bo%2BmoE1npU" rel="nofollow">一文读懂HTTP/2 及 HTTP/3特性</a></li>
</ul>
面试经典之http中get与post的区别
https://segmentfault.com/a/1190000018799171
2019-04-09T06:58:46+08:00
2019-04-09T06:58:46+08:00
Alan
https://segmentfault.com/u/xc_xiang
13
<p><code>GET</code>和<code>POST</code>是<code>HTTP</code>请求的两种基本方法,要说它们的区别,接触过<code>WEB</code>开发的人都能说出一二。<br>最直观的区别就是<code>GET</code>把参数包含在<code>URL</code>中,<code>POST</code>通过<code>request body</code>传递参数。</p>
<p>你可能自己写过无数个<code>GET</code>和<code>POST</code>请求,或者已经看过很多权威网站总结出的他们的区别,你非常清楚知道什么时候该用什么。</p>
<h2>1、常规答案</h2>
<p>当你在面试中被问到这个问题,你的内心充满了自信和喜悦。你轻轻松松的给出了一个“标准答案”:</p>
<table>
<thead><tr>
<th>-</th>
<th>GET</th>
<th>POST</th>
</tr></thead>
<tbody>
<tr>
<td>后退按钮/刷新</td>
<td>无害</td>
<td>数据会被重新提交(浏览器应该告知用户数据会被重新提交)。</td>
</tr>
<tr>
<td>书签</td>
<td>可收藏为书签</td>
<td>不可收藏为书签</td>
</tr>
<tr>
<td>缓存</td>
<td>能被缓存</td>
<td>不能缓存</td>
</tr>
<tr>
<td>编码类型</td>
<td>
<code>application/x-www-form-urlencoded</code> 只能进行url编码</td>
<td>
<code>application/x-www-form-urlencoded</code> 或 <code>multipart/form-data</code>。为二进制数据使用多重编码。</td>
</tr>
<tr>
<td>历史</td>
<td>参数保留在浏览器历史中。</td>
<td>参数不会保存在浏览器历史中。</td>
</tr>
<tr>
<td>对数据长度的限制</td>
<td>是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 个字符)。</td>
<td>无限制。</td>
</tr>
<tr>
<td>对数据类型的限制</td>
<td>只允许 ASCII 字符。</td>
<td>没有限制。也允许二进制数据。</td>
</tr>
<tr>
<td>安全性</td>
<td>与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。在发送密码或其他敏感信息时绝不要使用 GET !</td>
<td>POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。</td>
</tr>
<tr>
<td>可见性</td>
<td>数据在 URL 中对所有人都是可见的。</td>
<td>数据不会显示在 URL 中。</td>
</tr>
</tbody>
</table>
<p>“很遗憾,这不是我们要的回答!”</p>
<p>请告诉我真相。。。</p>
<h2>2、本质区别</h2>
<p>如果我告诉你<strong>GET和POST本质上没有区别</strong>你信吗? </p>
<p>让我们扒下GET和POST的外衣,坦诚相见吧!</p>
<p>GET和POST是什么?HTTP协议中的两种发送请求的方法。</p>
<p>HTTP是什么?HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。</p>
<p>HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。</p>
<h3>2.1 那么,“标准答案”里的那些区别是怎么回事?</h3>
<p>在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。</p>
<p>但是,我们只看到HTTP对GET和POST参数的传送渠道(url还是requrest body)提出了要求。</p>
<h3>2.2、“标准答案”里关于参数大小的限制又是从哪来的呢?</h3>
<p>在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然GET可以带request body,也不能保证一定能被接收到哦。</p>
<p>好了,现在你知道,<strong>GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。</strong></p>
<h3>2.3、最终boss</h3>
<p>你以为本文就这么结束了?<br>我们的大BOSS还等着出场呢。。。<br>这位BOSS有多神秘?当你试图在网上找“GET和POST的区别”的时候,那些你会看到的搜索结果里,从没有提到他。他究竟是什么呢。。。</p>
<p>GET和POST还有一个重大区别,简单的说:</p>
<p><strong>GET产生一个TCP数据包;POST产生两个TCP数据包。</strong><br>也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。</p>
<p>因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?</p>
<ol>
<li>GET与POST都有自己的语义,不能随便混用。</li>
<li>据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。</li>
<li>并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。</li>
</ol>
<p>好了,看到这里之后再也不怕面试官问get与post请求的区别了。</p>
React源码系列一之createElement
https://segmentfault.com/a/1190000018776289
2019-04-06T14:41:33+08:00
2019-04-06T14:41:33+08:00
Alan
https://segmentfault.com/u/xc_xiang
14
<p>前言:使用react也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。</p>
<blockquote>备注:react和react-dom源码版本为16.8.6 本文适合使用过React进行开发,并有一定经验的人阅读。</blockquote>
<p>好了闲话少说,我们一起来看源码吧<br>写过<code>react</code>知道,我们使用<code>react</code>编写代码都离不开<code>webpack</code>和<code>babel</code>,因为<code>React</code>要求我们使用的是<code>class</code>定义组件,并且使用了<code>JSX</code>语法编写<code>HTML</code>。浏览器是不支持<code>JSX</code>并且对于<code>class</code>的支持也不好,所以我们都是需要使用<code>webpack</code>的j<code>sx-loader</code>对<code>jsx</code>的语法做一个转换,并且对于<code>ES6</code>的语法和<code>react</code>的语法通过<code>babel</code>的<code>babel/preset-react</code>、<code>babel/env</code>和<code>@babel/plugin-proposal-class-properties</code>等进行转义。不熟悉怎么从头搭建<code>react</code>的我的<a href="https://link.segmentfault.com/?enc=1mvigoMrC2Wc7n82VZpJ7g%3D%3D.DylVYNcW8HUG7gK%2BwZHJhSZt%2Fj6c3F%2FQbRQxEWseki9vQyiD6B0jFJB2vG0MWm3%2BjboDIhL5iWw3Xe2aQqIIdYpnllIoNt4TTY%2BHs20qe%2BU%3D" rel="nofollow">示例代码</a>就放在这。</p>
<p>好了,我们从一个最简单实例<code>demo</code>来看<code>react</code>到底做了什么</p>
<h2>1、createElement</h2>
<p>下面是我们的代码</p>
<pre><code class="js">import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(
<h1 style={{color:'red'}} >11111</h1>,
document.getElementById("root")
);</code></pre>
<p>这是页面上的效果<br><img src="/img/bVbqV5d?w=369&h=206" alt="图片描述" title="图片描述"></p>
<p>我们现在看看在浏览器中的代码是如何实现的:</p>
<pre><code class="js">react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
style: {
color: 'red'
}
}, "11111"), document.getElementById("root"));</code></pre>
<p>最终经过编译后的代码是这样的,发现原本的<code><h1>11111</h1></code>变成了一个<code>react.createElement</code>的函数,其中原生标签的类型,内容都变成了参数传入这个函数中.这个时候我们大胆的猜测<code>react.createElement</code>接受三个参数,分别是元素的类型、元素的属性、子元素。好了带着我们的猜想来看一下源码。</p>
<p>我们不难找到,源码位置在位置 <code>./node_modules/react/umd/react.development.js:1941</code></p>
<pre><code class="js">function createElement(type, config, children) {
var propName = void 0;
// Reserved names are extracted
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
{
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
{
if (key || ref) {
var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}</code></pre>
<p>首先我们来看一下它的三个参数<br>第一个<code>type</code>:我们想一下这个<code>type</code>的可能取值有哪些?</p>
<ul>
<li>第一种就是我们上面写的原生的标签类型(例如<code>h1</code>、<code>div</code>,<code>span</code>等);</li>
<li>第二种就是我们React组件了,就是这面这种<code>App</code>
</li>
</ul>
<pre><code class="js">class App extends React.Component {
static defaultProps = {
text: 'DEMO'
}
render() {
return (<h1>222{this.props.text}</h1>)
}
}</code></pre>
<p>第二个<code>config</code>:这个就是我们传递的一些属性<br>第三个<code>children</code>:这个就是子元素,最开始我们猜想就三个参数,其实后面看了源码就知道这里其实不止三个。</p>
<p>接下来我们来看看<code>react.createElement</code>这个函数里面会帮我们做什么事情。<br>1、首先会初始化一些列的变量,之后会判断我们传入的元素中是否带有有效的<code>key</code>和<code>ref</code>的属性,这两个属性对于<code>react</code>是有特殊意义的(key是可以优化React的渲染速度的,ref是可以获取到React渲染后的真实DOM节点的),如果检测到有传入<code>key</code>,<code>ref</code>,<code>__self</code>和<code>__source</code>这4个属性值,会将其保存起来。</p>
<p>2、接着对传入的<code>config</code>做处理,遍历<code>config</code>对象,并且剔除掉4个内置的保留属性<code>(key,ref,__self,__source)</code>,之后重新组装新的<code>config</code>为<code>props</code>。这个<code>RESERVED_PROPS</code>是定义保留属性的地方。</p>
<pre><code class="js"> var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
};</code></pre>
<p>3、之后会检测传入的参数的长度,如果<code>childrenLength</code>等于1的情况下,那么就代表着当前<code>createElement</code>的元素只有一个子元素,那么将内容赋值到<code>props.children</code>。那什么时候<code>childrenLength</code>会大于1呢?那就是当你的元素里面涉及到多个子元素的时候,那么<code>children</code>将会有多个传入到<code>createElement</code>函数中。例如:</p>
<pre><code class="js"> ReactDOM.render(
<h1 style={{color:'red'}} key='22'>
<div>111</div>
<div>222</div>
</h1>,
document.getElementById("root")
);</code></pre>
<p>编译后是什么样呢?</p>
<pre><code class="js"> react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
style: {
color: 'red'
},
key: "22"
},
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "111"),
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "222")),
document.getElementById("root")
);</code></pre>
<p>这个时候react.createElement拿到的<code>arguments.length</code>就大于3了。也就是<code>childrenLength</code>大于1。这个时候我们就遍历把这些子元素添加到<code>props.children</code>中。<br>4、接着函数将会检测是否存在<code>defaultProps</code>这个参数,因为现在的是一个最简单的demo,而且传入的只是原生元素,所以没有<code>defaultProps</code>这个参数。那么我们来看下面的例子:</p>
<pre><code class="js"> import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
static defaultProps = {
text: '33333'
}
render() {
return (<h1>222{this.props.text}</h1>)
}
}
ReactDOM.render(
<App/>,
document.getElementById("root")
);</code></pre>
<p>编译后的</p>
<pre><code class="js"> var App =
/*#__PURE__*/
function (_Component) {
_inherits(App, _Component);
function App() {
_classCallCheck(this, App);
return _possibleConstructorReturn(this, _getPrototypeOf(App).apply(this, arguments));
}
_createClass(App, [{
key: "render",
value: function render() {
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", null, "222", this.props.text);
}
}]);
return App;
}(react__WEBPACK_IMPORTED_MODULE_0__["Component"]);
_defineProperty(App, "defaultProps", {
text: '33333'
});
react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(App, null),
document.getElementById("root")
);</code></pre>
<p>发现传入react.createElement的是一个App的函数,class经过babel转换后会变成一个构造函数。有兴趣可以自己去看babel对于class的转换,这里就不解析转换过程,总得来说就是返回一个App的构造函数传入到react.createElement中.如果<code>type</code>传的东西是个对象,且<code>type</code>有<code>defaultProps</code>这个东西并且<code>props</code>中对应的值是<code>undefined</code>,那就<code>defaultProps</code>的值也塞<code>props</code>里面。这就是我们组价默认属性的由来。</p>
<p>5、 检测<code>key</code>和<code>ref</code>是否有赋值,如果有将会执行<code>defineKeyPropWarningGetter</code>和<code>defineRefPropWarningGetter</code>两个函数。</p>
<pre><code class="js">function defineKeyPropWarningGetter(props, displayName) {
var warnAboutAccessingKey = function () {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
warningWithoutStack$1(false, '%s: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, 'key', {
get: warnAboutAccessingKey,
configurable: true
});
}
function defineRefPropWarningGetter(props, displayName) {
var warnAboutAccessingRef = function () {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
warningWithoutStack$1(false, '%s: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName);
}
};
warnAboutAccessingRef.isReactWarning = true;
Object.defineProperty(props, 'ref', {
get: warnAboutAccessingRef,
configurable: true
});
}</code></pre>
<p>我么可以看出这个二个方法就是给<code>key</code>和<code>ref</code>添加了警告。这个应该只是在开发环境才有其中<code>isReactWarning</code>就是上面判断<code>key</code>与<code>ref</code>是否有效的一个标记。<br>6、最后将一系列组装好的数据传入<code>ReactElement</code>函数中。</p>
<h2>2、ReactElement</h2>
<pre><code class="js">var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner
};
{
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false
});
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};</code></pre>
<p>其实里面非常简单,就是将传进来的值都包装在一个element对象中</p>
<ul><li>$$typeof:其中REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement</li></ul>
<pre><code class="js">var hasSymbol = typeof Symbol === 'function' && Symbol.for;
var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;</code></pre>
<p>从代码上看如果支持<code>Symbol</code>就会用<code>Symbol.for</code>方法创建一个<code>key</code>为<code>react.element</code>的<code>symbol</code>,否则就会返回一个<code>0xeac7</code></p>
<ul>
<li>type -> tagName或者是一个函数</li>
<li>key -> 渲染元素的key</li>
<li>ref -> 渲染元素的ref</li>
<li>props -> 渲染元素的props</li>
<li>_owner -> Record the component responsible for creating this element.(记录负责创建此元素的组件,默认为null)</li>
<li>_store -> 新的对象</li>
</ul>
<p><code>_store</code>中添加了一个新的对象<code>validated</code>(可写入),<br><code>element</code>对象中添加了<code>_self</code>和<code>_source</code>属性(只读),最后冻结了<code>element.props</code>和<code>element</code>。<br>这样就解释了为什么我们在子组件内修改<code>props</code>是没有效果的,只有在父级修改了<code>props</code>后子组件才会生效</p>
<p>最后就将组装好的<code>element</code>对象返回了出来,提供给<code>ReactDOM.render</code>使用。到这有关的主要内容我们看完了。下面我们来补充一下知识点</p>
<h3>Object.freeze</h3>
<p><code>Object.freeze</code>方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。</p>
<pre><code class="js">const obj = {
a: 1,
b: 2
};
Object.freeze(obj);
obj.a = 3; // 修改无效</code></pre>
<p>需要注意的是冻结中能冻结当前对象的属性,如果obj中有一个另外的对象,那么该对象还是可以修改的。所以React才会需要冻结element和element.props。</p>
<pre><code class="js">if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}</code></pre>
<blockquote>后续更多文章将在我的<a href="https://link.segmentfault.com/?enc=JIZyFV0SWKsxAzWPOBw73w%3D%3D.9snbCJSLthjBZWt02vfm2qnU%2FH4cbrKlpTV8v7k0y6a%2F%2BeHbt%2FEemulw442nEaz9" rel="nofollow">github</a>第一时间发布,欢迎关注。</blockquote>
箭头函数你想知道的都在这里
https://segmentfault.com/a/1190000018760341
2019-04-04T08:43:22+08:00
2019-04-04T08:43:22+08:00
Alan
https://segmentfault.com/u/xc_xiang
41
<h2>1、基本语法回顾</h2>
<p>我们先来回顾下箭头函数的基本语法。<br>ES6 增加了箭头函数:</p>
<pre><code class="js">var f = v => v;
// 等同于
var f = function (v) {
return v;
};</code></pre>
<p>如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。</p>
<pre><code class="js">var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};</code></pre>
<p>由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。</p>
<pre><code class="js">// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });</code></pre>
<p>下面是一种特殊情况,虽然可以运行,但会得到错误的结果。</p>
<pre><code class="js">let foo = () => { a: 1 };
foo() // undefined</code></pre>
<p>上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。</p>
<p>关于作用域</p>
<pre><code class="js">箭头函数内定义的变量及其作用域
// 常规写法
var greeting = () => {let now = new Date(); return ("Good" + ((now.getHours() > 17) ? " evening." : " day."));}
greeting(); //"Good day."
console.log(now); // ReferenceError: now is not defined 标准的let作用域
// 参数括号内定义的变量是局部变量(默认参数)
var greeting = (now=new Date()) => "Good" + (now.getHours() > 17 ? " evening." : " day.");
greeting(); //"Good day."
console.log(now); // ReferenceError: now is not defined
// 对比:函数体内{}不使用var定义的变量是全局变量
var greeting = () => {now = new Date(); return ("Good" + ((now.getHours() > 17) ? " evening." : " day."));}
greeting(); //"Good day."
console.log(now); // Fri Dec 22 2017 10:01:00 GMT+0800 (中国标准时间)
// 对比:函数体内{} 用var定义的变量是局部变量
var greeting = () => {var now = new Date(); return ("Good" + ((now.getHours() > 17) ? " evening." : " day."));}
greeting(); //"Good day."
console.log(now); // ReferenceError: now is not defined</code></pre>
<h2>2、关于this</h2>
<h3>2.1、默认绑定外层this</h3>
<p><strong>箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。</strong><br>这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。</p>
<pre><code class="js">function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42</code></pre>
<p>上面代码中,<code>setTimeout</code>的参数是一个箭头函数,这个箭头函数的定义生效是在<code>foo</code>函数生成时,而它的真正执行要等到 <code>100 </code>毫秒后。如果是普通函数,执行时<code>this</code>应该指向全局对象<code>window</code>,这时应该输出<code>21</code>。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是<code>{id: 42}</code>),所以输出的是<code>42</code>。<br>箭头函数可以让<code>setTimeout</code>里面的<code>this</code>,绑定定义时所在的作用域,而不是指向运行时所在的作用域。</p>
<p>所以,箭头函数转成 ES5 的代码如下。</p>
<pre><code class="js">// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}</code></pre>
<h3>2.2、 不能用<code>call()、apply()、bind()</code>方法修改里面的this</h3>
<pre><code class="js">(function() {
return [
(() => this.x).bind({ x: 'inner' })() // 无效的bind,最终this还是指向外层
];
}).call({ x: 'outer' });
// ['outer']</code></pre>
<p>上面代码中,箭头函数没有自己的<code>this</code>,所以<code>bind</code>方法无效,内部的<code>this</code>指向外部的<code>this</code>。</p>
<h2>3、没有 arguments</h2>
<p>箭头函数没有自己的 <code>arguments</code> 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 <code>arguments</code> 对象:</p>
<pre><code class="js">function constant() {
return () => arguments[0]
}
var result = constant(1);
console.log(result()); // 1</code></pre>
<p>那如果我们就是要访问箭头函数的参数呢?</p>
<p>你可以通过命名参数或者 rest 参数的形式访问参数:</p>
<pre><code class="js">let nums = (...nums) => nums;</code></pre>
<h2>4、 不能通过 new 关键字调用</h2>
<p><code>JavaScript </code>函数有两个内部方法:<code>[[Call]]</code> 和 <code>[[Construct]]</code>。</p>
<p>当通过<code>new</code>调用函数时,执行<code>[Construct]]</code>方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。</p>
<p>当直接调用的时候,执行<code>[[Call]]</code>方法,直接执行函数体。</p>
<p>箭头函数并没有<code>[[Construct]]</code>方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。</p>
<pre><code class="js">var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor</code></pre>
<h2>5、没有原型</h2>
<p>由于不能使用<code>new</code>调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在<code>prototype</code>这个属性。</p>
<pre><code class="js">var Foo = () => {};
console.log(Foo.prototype); // undefined</code></pre>
<h2>5、不适用场合</h2>
<p>第一个场合是定义函数的方法,且该方法内部包括this。</p>
<pre><code class="js">const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}</code></pre>
<p>上面代码中,<code>cat.jumps()</code>方法是一个箭头函数,这是错误的。调用<code>cat.jumps()</code>时,如果是普通函数,该方法内部的<code>this</code>指向<code>cat</code>;如果写成上面那样的箭头函数,使得<code>this</code>指向全局对象,因此不会得到预期结果。<br>第二个场合是需要动态this的时候,也不应使用箭头函数。</p>
<pre><code class="js">var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});</code></pre>
<p>上面代码运行时,点击按钮会报错,因为<code>button</code>的监听函数是一个箭头函数,导致里面的<code>this</code>就是全局对象。如果改成普通函数,<code>this</code>就会动态指向被点击的按钮对象。</p>
<h2>6、使用场景</h2>
<p>下面这个是我们开发经常遇到的。我们一般会通过this赋值给一个变量,然后再通过变量访问。</p>
<pre><code class="js">class Test {
constructor() {
this.birth = 10;
}
submit(){
let self = this;
$.ajax({
type: "POST",
dataType: "json",
url: "xxxxx" ,//url
data: "xxxxx",
success: function (result) {
console.log(self.birth);//10
},
error : function() {}
});
}
}
let test = new Test();
test.submit();//undefined </code></pre>
<p>这里我们就可以通过箭头函数来解决</p>
<pre><code class="js">...
success: (result)=> {
console.log(this.birth);//10
},
...</code></pre>
<p>箭头函数在<code>react</code>中的运用场景</p>
<pre><code class="js">class Foo extends Component {
constructor(props) {
super(props);
}
handleClick() {
console.log('Click happened', this);
this.setState({a: 1});
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}</code></pre>
<p>在react中我们这样直接调用方法是有问题的,在<code>handleClick</code>函数中的this是有问题,我们平时需要这么做</p>
<pre><code class="js">class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click happened', this);
this.setState({a: 1});
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}</code></pre>
<p>这里通过<code>this.handleClick.bind(this)</code>给函数绑定<code>this</code>。但是这样写起来有些麻烦,有没有简单的方法呢?这时候我们的箭头函数就出场了</p>
<pre><code class="js">class Foo extends Component {
// Note: this syntax is experimental and not standardized yet.
handleClick = () => {
console.log('Click happened', this);
this.setState({a: 1});
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}</code></pre>
<p>箭头函数中 this 的值是继承自 外围作用域,很好的解决了这个问题。<br>除此之外我们还可以用箭头函数传参(这个不是必须的),而且会有性能问题。<a href="https://link.segmentfault.com/?enc=AWlDcZcndjEs6RXrBtPMMg%3D%3D.Fx3%2Ftea%2B1%2Bs0Cl4qDQrtNB6o3YGjTg2uOqVqAUqFSJcNGMl3%2FhQ10ttsHTeFGQjh" rel="nofollow">更多信息请查看</a></p>
<pre><code class="js">const A = 65 // ASCII character code
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i)).
};
}
handleClick(letter) {
this.setState({ justClicked: letter });
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}</code></pre>
<p>最后<br>更多系列文章请看</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=%2B7CVNSWhb4IScy2fAKSsxg%3D%3D.BX9qiYHhwsko7DjOsj%2FamhN6eQ%2BjBveT9udH8%2FaXyx3d79%2Bw2Hjg5bpNHD75uMlq" rel="nofollow">ES6学习(一)之var、let、const</a></li>
<li><a href="https://link.segmentfault.com/?enc=q0G4ywxl221ai7AjzEf7Hg%3D%3D.mO%2FUouuWP%2BgVgSz7oxxES0t0fn41XzyFuF035cRjAkCRbvBDNUa2P9MJYsr0VhAI" rel="nofollow">ES6学习(二)之解构赋值及其原理</a></li>
<li><a href="https://link.segmentfault.com/?enc=SGHRMCRTIWrj33NDjW9s2A%3D%3D.3zLxMzMnPBhLgWb3v%2BKJDkFKfHtUAWn7UgtOhDeGpfqYLWKFi267sYN552AGKe5%2F" rel="nofollow">ES6学习(三)之Set的模拟实现</a></li>
<li><a href="https://link.segmentfault.com/?enc=8AtnsMLAoKBxa1Yp7JK7gw%3D%3D.DX12yAaECfF%2BHwuG55r1TYK%2B5FRtDGfcqqA2Vfs7EfcXQUf466uRMGU6NtoqP5aN" rel="nofollow">ES6学习(四)之Promise的模拟实现</a></li>
</ul>
<blockquote>如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎<a href="https://link.segmentfault.com/?enc=LE0UZq3TT31sOiC0aa6Luw%3D%3D.AUkYcQEMm2CY9Pfzq1TvEDh61%2BS8iOEopEd9druqJU4BTQwYVp1u30mQZnlpxHJx" rel="nofollow">star</a>对作者也是一种鼓励。</blockquote>
面试官问你基本类型时他想知道什么
https://segmentfault.com/a/1190000018745719
2019-04-02T22:18:52+08:00
2019-04-02T22:18:52+08:00
Alan
https://segmentfault.com/u/xc_xiang
61
<blockquote>前言: 面试的时候我们经常会被问答js的数据类型。大部分情况我们会这样回答包括<br>1、基本类型(值类型或者原始类型): Number、Boolean、String、NULL、Undefined以及ES6的Symbol<br>2、引用类型:Object、Array、Function、Date等<br>我曾经也是这样回答的,并且一直觉得没有什么问题。</blockquote><h2>1 、在内存中的位置不同</h2><ul><li>基本类型: 占用空间固定,保存在<strong>栈中</strong>;</li><li>引用类型:占用空间不固定,保存在<strong>堆中</strong>;</li></ul><blockquote><strong>栈(stack)</strong>为自动分配的内存空间,它由系统自动释放;使用一级缓存,被调用时通常处于存储空间中,调用后被立即释放。<br><strong>堆(heap)</strong>则是动态分配的内存,大小不定也不会自动释放。使用二级缓存,生命周期与虚拟机的GC算法有关</blockquote><p>当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;栈中存储的是基础变量以及一些对象的引用变量,基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。</p><p>当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。</p><h2>2、赋值、浅拷贝、深拷贝</h2><ul><li>对于基本类型值,赋值、浅拷贝、深拷贝时都是复制基本类型的值给新的变量,之后二个变量之间操作不在相互影响。</li><li><p>对于引用类型值,</p><ul><li><strong>赋值</strong>后二个变量指向同一个地址,一个变量改变时,另一个也同样改变;</li><li><strong>浅拷贝</strong>后得到一个新的变量,这个与之前的已经不是指向同一个变量,改变时不会使原数据中的基本类型一同改变,但会改变会原数据中的引用类型数据</li><li><strong>深拷贝</strong>后得到的是一个新的变量,她的改变不会影响元数据</li></ul></li></ul><table><thead><tr><th>-</th><th>和原数据是否指向同一对象</th><th>第一层数据为基本数据类型</th><th>原数据中包含子对象</th></tr></thead><tbody><tr><td>赋值</td><td>是</td><td>改变会使原数据一同改变</td><td>改变会使原数据一同改变</td></tr><tr><td>浅拷贝</td><td>否</td><td>改变不会使原数据一同改变</td><td>改变会使原数据一同改变</td></tr><tr><td>深拷贝</td><td>否</td><td>改变不会使原数据一同改变</td><td>改变不会使原数据一同改变</td></tr></tbody></table><pre><code class="js"> var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};</code></pre><h3>2.1、浅拷贝</h3><p><strong>数组常用的浅拷贝方法</strong>有<code>slice</code>,<code>concat</code>,<code>Array.from() </code>,以及es6的析构</p><pre><code class="js">var arr1 = [1, 2,{a:1,b:2,c:3,d:4}];
var arr2 = arr1.slice();
var arr3 = arr1.concat();
var arr4 = Array.from(arr1);
var arr5 = [...arr1];
arr2[0]=2;
arr2[2].a=2;
arr3[0]=3;
arr3[2].b=3;
arr4[0]=4;
arr4[2].c=4;
arr5[0]=5;
arr5[2].d=5;
// arr1[1,2,{a:2,b:3,c:4,d:5}]
// arr2[2,2,{a:2,b:3,c:4,d:5}]
// arr3[3,2,{a:2,b:3,c:4,d:5}]
// arr4[4,2,{a:2,b:3,c:4,d:5}]
// arr5[5,2,{a:2,b:3,c:4,d:5}]</code></pre><p>对象常用的浅拷贝方法<code>Object.assign()</code>,es6析构</p><pre><code class="js">var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.x=2;
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}</code></pre><p>我们自己实现一个浅拷贝</p><pre><code class="js">var obj = { a:1, arr: [2,3] };
var shallowObj = shallowCopy(obj);
var shallowCopy = function(obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}</code></pre><h3>2.2、深拷贝</h3><p>比较简单粗暴的的做法是使用<code>JSON.parse(JSON.stringify(obj))</code></p><pre><code class="js">var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
var new_arr = JSON.parse( JSON.stringify(arr) );
new_arr[4].old=4;
console.log(arr); //['old', 1, true, ['old1', 'old2'], {old: 1}]
console.log(new_arr); //['old', 1, true, ['old1', 'old2'], {old: 4}]</code></pre><p><code>JSON.parse(JSON.stringify(obj))</code> 看起来很不错,不过<a href="https://link.segmentfault.com/?enc=AgZzkrr%2BD9SndNswPZJliQ%3D%3D.20o4hUtUmGhwgUtWTRzoLdTm7bAsspnvu25Jt8gsghfRnhciDZgOK3wKd7sBGej9pjZ7qEjBiH3glNdlvQ94pReAMYdVF%2BROsQdtnYDOi6fkwjwKYrXe0Tpwu6b1zpJ1" rel="nofollow">MDN文档</a> 的描述有句话写的很清楚:</p><blockquote>undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。</blockquote><p>但是在平时的开发中<code>JSON.parse(JSON.stringify(obj))</code>已经满足90%的使用场景了。<br>下面我们自己来实现一个</p><pre><code class="js">var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}</code></pre><h2>3、参数的传递</h2><p>所有的函数参数都是按值传递。也就是说把函数外面的值赋值给函数内部的参数,就和把一个值从一个变量赋值给另一个一样;</p><ul><li>基本类型</li></ul><pre><code class="js">var a = 2;
function add(x) {
return x = x + 2;
}
var result = add(a);
console.log(a, result); // 2 4
</code></pre><p>引用类型</p><pre><code class="js">function setName(obj) {
obj.name = 'laowang';
obj = new Object();
obj.name = 'Tom';
}
var person = new Object();
setName(person);
console.log(person.name); //laowang</code></pre><p>很多人错误地以为在局部作用域中修改的对象在全局作用域中反映出来就是说明参数是按引用传递的。<br>但是通过上面的例子可以看出如果person是按引用传递的最终的person.name应该是Tom。<br>实际上当函数内部重写obj时,这个变量引用的就是一个局部变量了。而这个变量会在函数执行结束后销毁。(这是是在js高级程序设计看到的,还不是很清楚)</p><h2>4、判断方法</h2><p>基本类型用<code>typeof</code>,引用类型用<code>instanceof</code></p><blockquote>特别注意<code>typeof null</code>是<code>"object"</code>, <code>null instanceof Object</code>是<code>false</code>;</blockquote><pre><code class="js">console.log(typeof "Nicholas"); // "string"
console.log(typeof 10); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"
var items = [];
var obj = {};
function reflect(value){
return value;
}
console.log(items instanceof Array); // true;
console.log(obj instanceof Object); // true;
console.log(reflect instanceof Function); // true;
</code></pre><blockquote><code>Object.prototype.toString.call([]).slice(8, -1)</code>有兴趣的同学可以看一下这个是干什么的</blockquote><h2>5、总结</h2><p><img src="/img/bVbqOL9" alt="图片描述" title="图片描述"></p><blockquote>如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎<a href="https://link.segmentfault.com/?enc=aTVOAiQLxN8Ck2A84Jh1qw%3D%3D.I7KxSJIU0i8y4xTyXu4LJT%2Fs%2BAYnUs41%2BQrgiDhKtBFW2FnLRilYUc5QrwrlhZ9x" rel="nofollow">star</a>对作者也是一种鼓励。</blockquote>
Cookie和Session你不能不知道的秘密
https://segmentfault.com/a/1190000018733119
2019-04-01T22:39:03+08:00
2019-04-01T22:39:03+08:00
Alan
https://segmentfault.com/u/xc_xiang
4
<h2>1、cookie是什么</h2>
<p>由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。</p>
<p>Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。如下图:<br><img src="/img/bVbqLiT?w=1076&h=540" alt="图片描述" title="图片描述"></p>
<h3>1.1、cookie的属性</h3>
<table>
<thead><tr>
<th>属性项</th>
<th>属性项介绍</th>
</tr></thead>
<tbody>
<tr>
<td>Name</td>
<td>一个唯一确定cookie的名称(cookie名称不区分大小写,实践中最好区分,因为一些服务器会区分),URL编码</td>
</tr>
<tr>
<td>Value</td>
<td>存储cookie的字符串值,值必须经过URL编码</td>
</tr>
<tr>
<td>Expires</td>
<td>过期时间,在这个时间点后Cookie失效</td>
</tr>
<tr>
<td>Domain</td>
<td>生成Cookie域名</td>
</tr>
<tr>
<td>Path</td>
<td>该Cookie是在当前那个路径下生成的</td>
</tr>
<tr>
<td>Secure</td>
<td>加密设置,设置他之后,只会在SSL连接时才会回传该Cookie</td>
</tr>
</tbody>
</table>
<p><img src="/img/bVbqLmM?w=1542&h=440" alt="图片描述" title="图片描述"><br>这里我只要说二个点:<br>1、Expires:该Cookie失效的时间,单位秒。</p>
<ul>
<li>如果为正数,则该Cookie在maxAge秒之后失效(<strong>持久级别Cookie</strong>)。</li>
<li>如果为负数,该Cookie为临时Cookie,关闭浏览器即失效(<strong>会话级别Cookie</strong>),浏览器也不会以任何形式保存该Cookie。</li>
<li>如果为0,表示删除该Cookie。默认为–1;</li>
</ul>
<p>2、Domain: <br>我们现在有二个域名。域名A:b.f.com,域名B:d.f.com;显然域名A和域名B都是f.com的子域名</p>
<ul><li>如果我们在域名A中的Cookie的domain设置为.f.com,那么.f.com及其子域名都可以获取这个Cookie,即域名A和域名B都可以获取这个Cookie</li></ul>
<p>如果域名A没有显式设置Cookie的domain方法,那么domain就为.b.f.com,不一样的是,这时,域名A的子域名将无法获取这个Cookie</p>
<blockquote>HttpOnly: 这个属性是面试的时候常考的,如果这个属性设置为true,就不能通过js脚本来获取cookie的值,能有效的防止xss攻击</blockquote>
<h3>1.2、cookie的操作</h3>
<p>由于js没有给原生的操作方法,我们可以简单地封装一下:</p>
<pre><code class="js">var cookieUtil = {
getItem: function (name) {
var cookieName = encodeURIComponent(name) + "=",
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null;
if (cookieStart > -1) {
var cookieEnd = document.cookie.indexOf(';', cookieStart);
if (cookieEnd == 1) {
cookieEnd = document.cookie.length;
}
cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd))
}
return cookieValue;
},
setItem: function (name, value, expires, path, domain, secure) {
var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value);
if (expires) {
cookieText += ";expires=" + expires.toGMTString();
}
if (path) {
cookieText += ";path=" + path;
}
if (domain) {
cookieText += ";domain=" + domain;
}
if (secure) {
cookieText += ";secure";
}
document.cookie = cookieText;
},
unset: function (name, path, domain, secure) {
this.setItem(name, "", new Date(0), path, domain, secure)
}
}
CookieUtil.setItem("name", 'tom'); // 设置cookie
console.log(CookieUtil.getItem('name'));//读取cookie
CookieUtil.unset("name")//删除cookie</code></pre>
<h3>1.3、Cookie防篡改机制</h3>
<p>因为Cookie是存储在客户端,用户可以随意修改。所以,存在一定的安全隐患。</p>
<blockquote>防篡改签名:服务器为每个Cookie项生成签名。如果用户篡改Cookie,则与签名无法对应上。以此,来判断数据是否被篡改。</blockquote>
<p>原理如下:</p>
<ul>
<li>服务端提供一个签名生成算法secret</li>
<li>根据方法生成签名secret(wall)=34Yult8i</li>
<li>将生成的签名放入对应的Cookie项username=wall|34Yult8i。其中,内容和签名用|隔开。</li>
<li>服务端根据接收到的内容和签名,校验内容是否被篡改。</li>
</ul>
<p>举个栗子:<br>比如服务器接收到请求中的Cookie项username=pony|34Yult8i,然后使用签名生成算法secret(pony)=666。 算法得到的签名666和请求中数据的签名不一致,则证明数据被篡改。</p>
<h2>2、Session</h2>
<p>Session: 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中.为了获得更高的存取速度,服务器一般把Session放在内存里。每个用户都会有一个独立的Session。如果Session内容过于复杂,当大量客户访问服务器时可能会导致内存溢出。因此,Session里的信息应该尽量精简。</p>
<p>当客户端请求创建一个session的时候,服务器会先检查这个客户端的请求里是否已包含了一个session标识 - sessionId,</p>
<ul>
<li>如果已包含这个sessionId,则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(如果检索不到,可能会新建一个)</li>
<li>如果客户端请求(一般是通过cookie携带)不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId</li>
</ul>
<p>sessionId的值一般是一个既不会重复,又不容易被仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。保存sessionId的方式大多情况下用的是cookie。</p>
<blockquote>session 的运行依赖 session id,而 session id 是存在 cookie中的</blockquote>
<p>如果客户端的浏览器禁用了 Cookie怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,(在 url 中传递 session_id)即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。</p>
<h2>3、cookie与session的区别</h2>
<table>
<thead><tr>
<th> </th>
<th>cookie</th>
<th>session</th>
</tr></thead>
<tbody>
<tr>
<td>存储位置</td>
<td>客户端</td>
<td>服务器端</td>
</tr>
<tr>
<td>存取方式</td>
<td>只能保管ASCII字符串</td>
<td>够存取任何类型的数据</td>
</tr>
<tr>
<td>有效期不同</td>
<td>Cookie可以设置过期时间属性</td>
<td>JSESSIONID的过期时间默许为–1,只需关闭了阅读器该Session就会失效</td>
</tr>
<tr>
<td>服务器压力</td>
<td>Cookie保管在客户端,不占用服务器资源。假如并发阅读的用户十分多,Cookie是很好的选择</td>
<td>Session是保管在服务器端的,每个用户都会产生一个Session。假如并发访问的用户十分多,会产生十分多的Session,耗费大量的内存</td>
</tr>
<tr>
<td>跨域支持上的不同</td>
<td>Cookie支持跨域名访问,例如将domain属性设置为“.biaodianfu.com”,则以“.biaodianfu.com”为后缀的一切域名均能够访问该Cookie。</td>
<td>Session则不会支持跨域名访问。Session仅在他所在的域名内有效。</td>
</tr>
<tr>
<td>区分路径</td>
<td>cookie中如果设置了路径参数,那么同一个网站中不同路径下的cookie互相是访问不到的</td>
<td>session不能区分路径,同一个用户在访问一个网站期间,所有的session在任何一个地方都可以访问到</td>
</tr>
</tbody>
</table>
<blockquote>如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎<a href="https://link.segmentfault.com/?enc=kIMB8%2FYDoPFO3skF2rccpg%3D%3D.DqLZUGRrcMJ7ZT%2B%2BSbiqIBdmwZB7ZZXT0EdBczpt9Hn92WTdL4kLrAavieN3EZvF" rel="nofollow">star</a>对作者也是一种鼓励。</blockquote>
<p>参考<br><a href="https://link.segmentfault.com/?enc=d7K%2Bc0p9RrAPV70dUb0qDg%3D%3D.G5Su3ZRQZpDGXbS6pcoJAu6YO%2BSAFn1WauH6cOvJN4OqqywS%2FXCLYSPfOrnGr73O" rel="nofollow">Cookie防篡改机制</a><br><a href="https://link.segmentfault.com/?enc=%2FnTsGkkG2P0JRbw0O2UH5g%3D%3D.g%2BfadBSNi8eqmGf7uYhAtZWQZQJCN5ltbFX9uq5qaJGENuG121R%2ByPPJTvcFYkJL" rel="nofollow">彻底理解cookie,session,token</a></p>
浏览器缓存看这一篇就够了
https://segmentfault.com/a/1190000018717463
2019-03-31T11:30:30+08:00
2019-03-31T11:30:30+08:00
Alan
https://segmentfault.com/u/xc_xiang
150
<blockquote>浏览器缓存作为性能优化的重要一环,对于前端而言,重要性不言而喻。以前总是一知半解的,所以这次好好整理总结了一下。</blockquote>
<h2>1、缓存机制</h2>
<p>首先我们来总体感知一下它的匹配流程,如下:</p>
<ol>
<li>浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。</li>
<li>没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。</li>
<li>如果前两步都没有命中,则直接从服务端获取资源。</li>
</ol>
<p><img src="/img/bVLR3B?w=692&h=1031" alt="图片描述" title="图片描述"></p>
<h2>2、强缓存</h2>
<p>强缓存:不会向服务器发送请求,直接从缓存中读取资源。</p>
<h3>2.1 强缓存原理</h3>
<p>强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:</p>
<ul><li>第一次请求,不存在缓存结果和缓存标识,直接向服务器发送请求</li></ul>
<p><img src="/img/bVbqHjA?w=630&h=361" alt="图片描述" title="图片描述"></p>
<ul><li>存在缓存标识和缓存结果,但是已经失效,强制缓存是啊比,则使用协商缓存(暂不分析)</li></ul>
<p><img src="/img/bVbqHjL?w=595&h=355" alt="图片描述" title="图片描述"></p>
<ul><li>存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果</li></ul>
<p><img src="/img/bVbqHj6?w=614&h=320" alt="图片描述" title="图片描述"></p>
<p>那么强制缓存的缓存规则是什么?<br>当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是<code>Expires</code>和<code>Cache-Control</code>,其中<code>Cache-Control</code>优先级比<code>Expires</code>高。</p>
<h4>2.1.1、 Expires</h4>
<p>缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,<code>Expires=max-age + 请求时间</code>,需要和<code>Last-modified</code>结合使用。<code>Expires</code>是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。</p>
<blockquote>Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。</blockquote>
<h4>2.1.2、 Cache-Control</h4>
<p>在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:</p>
<ul>
<li>public:所有内容都将被缓存(客户端和代理服务器都可缓存)</li>
<li>private:所有内容只有客户端可以缓存,<code>Cache-Control</code>的默认取值</li>
<li>no-cache:客户端缓存内容,但是是否使用缓存则需要经过<strong>协商缓存</strong>来验证决定</li>
<li>no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存</li>
<li>max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效</li>
</ul>
<blockquote>需要注意的是,<code>no-cache</code>这个名字有一点误导。设置了<code>no-cache</code>之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致,也就是协商缓存。而<code>no-store</code>才表示不会被缓存,即不使用强制缓存,也不使用协商缓存</blockquote>
<h4>2.1.3、设置</h4>
<p>强缓存需要服务端设置<code>expires</code>和<code>cache-control</code>。<br><code>nginx</code>代码参考,设置了一年的缓存时间:</p>
<pre><code>location ~ .*\.(ico|svg|ttf|eot|woff)(.*) {
proxy_cache pnc;
proxy_cache_valid 200 304 1y;
proxy_cache_valid any 1m;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale updating error timeout invalid_header http_500 http_502;
expires 1y;
}</code></pre>
<p>浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?这就是下面我们要讲到的<code>from disk cache</code>和<code>from memory cache</code>。</p>
<h3>2.2、from disk cache和from memory cache</h3>
<p>细心地同学在开发的时候应该注意到了Chrome的网络请求的Size会出现三种情况<code>from disk cache(磁盘缓存)</code>、<code>from memory cache(内存缓存)</code>、以及资源大小数值。</p>
<table>
<thead><tr>
<th>状态</th>
<th>类型</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>200</td>
<td>form memory cache</td>
<td>不请求网络资源,资源在内存当中,一般脚本、字体、图片会存在内存当中</td>
</tr>
<tr>
<td>200</td>
<td>form disk ceche</td>
<td>不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等</td>
</tr>
<tr>
<td>200</td>
<td>资源大小数值</td>
<td>从服务器下载最新资源</td>
</tr>
<tr>
<td>304</td>
<td>报文大小</td>
<td>请求服务端发现资源没有更新,使用本地资源</td>
</tr>
</tbody>
</table>
<p>浏览器读取缓存的顺序为memory –> disk。<br>以访问<code>https://github.com/xiangxingchen/blog</code>为例<br>我们第一次访问时<code>https://github.com/xiangxingchen/blog</code><br><img src="/img/bVbqHm2?w=1083&h=375" alt="图片描述" title="图片描述"><br>关闭标签页,再此打开<code>https://github.com/xiangxingchen/blog</code>时<br><img src="/img/bVbqHm4?w=1023&h=371" alt="图片描述" title="图片描述"><br>F5刷新时<br><img src="/img/bVbqHm8?w=1094&h=379" alt="图片描述" title="图片描述"></p>
<p>简单的对比一下<br><img src="/img/bVbqHny?w=1398&h=448" alt="图片描述" title="图片描述"></p>
<h2>3、协商缓存</h2>
<p>协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:</p>
<ul><li>协商缓存生效,返回304和Not Modified</li></ul>
<p><img src="/img/bVbqHn0?w=601&h=448" alt="图片描述" title="图片描述"></p>
<ul><li>协商缓存失效,返回200和请求结果</li></ul>
<p><img src="/img/bVbqHn7?w=588&h=422" alt="图片描述" title="图片描述"></p>
<h3>3.1、Last-Modified和If-Modified-Since</h3>
<ol>
<li>浏览器首先发送一个请求,让服务端在<code>response header</code>中返回请求的资源上次更新时间,就是<code>last-modified</code>,浏览器会缓存下这个时间。</li>
<li>然后浏览器再下次请求中,<code>request header</code>中带上<code>if-modified-since</code>:<code>[保存的last-modified的值]</code>。根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的响应,让浏览器中缓存中读取资源,这就大大减小了请求的消耗。</li>
</ol>
<p>由于last-modified依赖的是保存的绝对时间,还是会出现误差的情况:</p>
<ol>
<li>保存的时间是以秒为单位的,1秒内多次修改是无法捕捉到的;</li>
<li>各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用etag。</li>
</ol>
<h3>3.2、ETag和If-None-Match</h3>
<p><code>etag</code>是<code>http</code>协议提供的若干机制中的一种<code>Web</code>缓存验证机制,并且允许客户端进行缓存协商。生成etag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。 和<code>last-modified</code>一样.</p>
<ul>
<li>浏览器会先发送一个请求得到<code>etag</code>的值,然后再下一次请求在<code>request header</code>中带上<code>if-none-match</code>:<code>[保存的etag的值]</code>。</li>
<li>通过发送的<code>etag</code>的值和服务端重新生成的<code>etag</code>的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。</li>
</ul>
<blockquote>etag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。</blockquote>
<p>二者对比</p>
<ul>
<li>精确度上:<code>Etag</code>要优于<code>Last-Modified</code>。</li>
<li>优先级上:服务器校验优先考虑<code>Etag</code>。</li>
<li>性能上:<code>Etag</code>要逊于<code>Last-Modified</code>
</li>
</ul>
<h2>4、用户行为对浏览器缓存的影响</h2>
<ol>
<li>打开网页,地址栏输入地址: 查找 <code>disk cache</code> 中是否有匹配。如有则使用;如没有则发送网络请求。</li>
<li>普通刷新 (F5):因为 TAB 并没有关闭,因此 <code>memory cache</code> 是可用的,会被优先使用(如果匹配的话)。其次才是 <code>disk cache</code>。</li>
<li>强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 <code>Cache-control:no-cache</code>(为了兼容,还带了 <code>Pragma:no-cache</code>),服务器直接返回 200 和最新内容。</li>
</ol>
<h2>5、总结</h2>
<p><img src="/img/bVbqHqA?w=1564&h=1196" alt="图片描述" title="图片描述"></p>
<blockquote>如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎<a href="https://link.segmentfault.com/?enc=HxNDCXQjG6aHNuzp4vpTBw%3D%3D.m%2B6SQ9NCXL%2F4az390XUnG8xMon9atnz7IL4zqoIxqi3qLlWwdWNDivX1QRIpFSha" rel="nofollow">star</a>对作者也是一种鼓励。</blockquote>
手把手教你实现一个Promise
https://segmentfault.com/a/1190000018694544
2019-03-29T09:00:00+08:00
2019-03-29T09:00:00+08:00
Alan
https://segmentfault.com/u/xc_xiang
1
<h2>1、constructor</h2>
<p>首先我们都知道<code>Promise </code>有三个状态,为了方便我们把它定义成常量</p>
<pre><code class="js">const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';</code></pre>
<p>接下来我们来定义一个类</p>
<pre><code class="js">class MyPromise {
constructor(executor) {
//控制状态,使用了一次之后,接下来的都不被使用
this.state = PENDING;
this.value = null;
this.reason = null;
// 定义resolve函数
const resolve = value => {
if (this.state === PENDING) {
this.value = value;
this.state = FULFILLED;
}
}
// 定义reject函数
const reject = value => {
if (this.state === PENDING) {
this.reason = value;
this.state = REJECTED;
}
}
// executor方法可能会抛出异常,需要捕获
try {
// 将resolve和reject函数给使用者
executor(resolve, reject);
} catch (error) {
// 如果在函数中抛出异常则将它注入reject中
reject(error);
}
}
}</code></pre>
<p>到这基本比较好理解我简单说明一下</p>
<ul>
<li>
<code>executor</code>:这是实例<code>Promise</code>对象时在构造器中传入的参数,一般是一个<code>function(resolve,reject){}</code>
</li>
<li>
<code>state:</code>`Promise<code>的状态,一开始是默认的</code>pendding<code>状态,每当调用道</code>resolve<code>和</code>reject<code>方法时,就会改变其值,在后面的</code>then`方法中会用到</li>
<li>
<code>value</code>:<code>resolv</code>e回调成功后,调用<code>resolve</code>方法里面的参数值</li>
<li>
<code>reason</code>:<code>reject</code>回调成功后,调用<code>reject</code>方法里面的参数值</li>
<li>
<code>resolve</code>:声明<code>resolve</code>方法在构造器内,通过传入的<code>executor</code>方法传入其中,用以给使用者回调</li>
<li>
<code>reject</code>:声明<code>reject</code>方法在构造器内,通过传入的<code>executor</code>方法传入其中,用以给使用者回调</li>
</ul>
<h2>2、then</h2>
<p><code>then</code>就是将<code>Promise</code>中的<code>resolve</code>或者<code>reject</code>的结果拿到,那么我们就能知道这里的then方法需要两个参数,成功回调和失败回调,上代码!</p>
<pre><code class="js">then(onFulfilled, onRejected) {
if (this.state === FULFILLED) {
onFulfilled(this.value)
}
if (this.state === REJECTED) {
onRejected(this.reason)
}
}</code></pre>
<p>我们来简单的运行一下测试代码</p>
<pre><code class="js">const mp = new MyPromise((resolve, reject)=> {
resolve('******* i love you *******');
})
mp.then((suc)=> {
console.log(11111, suc);
}, (err)=> {
console.log('****** 你不爱我了*******', err)
})
// 11111 '******* i love you *******'</code></pre>
<p>这样看着好像没有问题,那么我们来试试异步函数呢?</p>
<pre><code class="js">const mp = new MyPromise((resolve, reject)=> {
setTimeout(()=> {
resolve('******* i love you *******');
}, 0)
})
mp.then((suc)=> {
console.log(11111, suc);
}, (err)=> {
console.log('****** 你不爱我了*******', err)
})</code></pre>
<p>我们会发现什么也没有打印,哪里出问题了呢?原来是由于异步的原因,当我们执行到<code>then</code>的时候<code>this. state</code>的值还没发生改变,所以<code>then</code>里面的判断就失效了。那么我们该怎么解决呢?</p>
<p>这就要说回经典得<code>callback </code>了。来上源码</p>
<pre><code class="js">// 存放成功回调的函数
this.onFulfilledCallbacks = [];
// 存放失败回调的函数
this.onRejectedCallbacks = [];
const resolve = value => {
if (this.state === PENDING) {
this.value = value;
this.state = FULFILLED;
this.onFulfilledCallbacks.map(fn => fn());
}
}
const reject = value => {
if (this.state === PENDING) {
this.value = value;
this.reason = REJECTED;
this.onRejectedCallbacks.map(fn => fn());
}
}</code></pre>
<p>在<code>then</code>里面添加</p>
<pre><code class="js">then(onFulfilled, onRejected) {
// ...
if(this.state === PENDING) {
this.onFulfilledCallbacks.push(()=> {
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(()=> {
onRejected(this.value);
})
}
}</code></pre>
<p>好了,到这异步的问题解决了,我们再来执行一下刚才的测试代码。结果就出来了。到这我们还缺什么呢?</p>
<ul>
<li>链式调用</li>
<li>当我们不传参数时应当什么运行</li>
</ul>
<p>这二个的思路也都很简单,链式调用也就是说我们再返回一个<code>promise</code>的实例就好了。而不传参则就是默认值的问题了。下面来看源码</p>
<pre><code class="js">then(onFulfilled, onRejected) {
let self = this;
let promise2 = null;
//解决onFulfilled,onRejected没有传值的问题
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y
//因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后then的resolve中捕获
onRejected = typeof onRejected === 'function' ? onRejected : err => {
throw err;
}
promise2 = new MyPromise((resolve, reject) => {
if (self.state === PENDING) {
console.log('then PENDING')
self.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
console.log(333333, x, typeof x);
self.resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
}, 0)
});
self.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(self.reason);
self.resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
}, 0);
});
}
if (self.state === FULFILLED) {
console.log('then FULFILLED')
setTimeout(() => {
try {
let x = onFulfilled(self.value);
self.resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
}, 0);
}
if (self.state === REJECTED) {
console.log('then REJECTED')
setTimeout(() => {
try {
let x = onRejected(self.reason);
self.resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
})
}
});
return promise2;
}</code></pre>
<p>为什么外面要包一层<code>setTimeout</code>?:因为<code>Promise</code>本身是一个异步方法,属于微任务一列,必须得在执行栈执行完了在去取他的值,所以所有的返回值都得包一层异步<code>setTimeout</code>。</p>
<p><code>resolvePromise</code>是什么?:这其实是官方<a href="https://link.segmentfault.com/?enc=88zU3pYyEmhXnbspiqR9Mw%3D%3D.b0EfNUvQg%2BkwY8ZY2FzwAhENMNk4AwAMbJzmx9mten8%3D" rel="nofollow">Promise/A+</a>的需求。因为你的<code>then</code>可以返回任何职,当然包括<code>Promise</code>对象,而如果是<code>Promise</code>对象,我们就需要将他拆解,直到它不是一个<code>Promise</code>对象,取其中的值。</p>
<h2>3、resolvePromise</h2>
<p>我们直接看代码</p>
<pre><code class="js">resolvePromise(promise2, x, resolve, reject) {
let self = this;
let called = false; // called 防止多次调用
//因为promise2是上一个promise.then后的返回结果,所以如果相同,会导致下面的.then会是同一个promise2,一直都是,没有尽头
//相当于promise.then之后return了自己,因为then会等待return后的promise,导致自己等待自己,一直处于等待
if (promise2 === x) {
return reject(new TypeError('循环引用'));
}
//如果x不是null,是对象或者方法
if (x !== null && (Object.prototype.toString.call(x) === '[object Object]' || Object.prototype.toString.call(x) === '[object Function]')) {
// x是对象或者函数
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, (y) => {
// 别人的Promise的then方法可能设置了getter等,使用called防止多次调用then方法
if (called) return;
called = true;
// 成功值y有可能还是promise或者是具有then方法等,再次resolvePromise,直到成功值为基本类型或者非thenable
self.resolvePromise(promise2, y, resolve, reject);
}, (reason) => {
if (called) return;
called = true;
reject(reason);
});
} else {
if (called) return;
called = true;
resolve(x);
}
} catch (reason) {
if (called) return;
called = true;
reject(reason);
}
} else {
// x是普通值,直接resolve
resolve(x);
}
}</code></pre>
<ul>
<li>为什么要在一开始判断<code>promise2</code>和<code>x</code>?:首先在<a href="https://link.segmentfault.com/?enc=%2BEj8gbgdPS%2BYfKZXggNpww%3D%3D.K6SeFHCnp2cuOpgb5V5m9VZebBlM%2FnigBEEpytmZf9c%3D" rel="nofollow">Promise/A+</a>中写了需要判断这两者如果相等,需要抛出异常,我就来解释一下为什么,如果这两者相等,我们可以看下下面的例子,第一次<code>p2</code>是<code>p1.then</code>出来的结果是个<code>Promise</code>对象,这个<code>Promise</code>对象在被创建的时候调用了<code>resolvePromise(promise2,x,resolve,reject)</code>函数,又因为<code>x</code>等于其本身,是个<code>Promise</code>,就需要<code>then</code>方法递归它,直到他不是<code>Promise</code>对象,但是<code>x(p2)</code>的结果还在等待,他却想执行自己的<code>then</code>方法,就会导致等待。</li>
<li>为什么要递归去调用<code>resolvePromise</code>函数?:相信细心的人已经发现了,我这里使用了递归调用法,首先这是<a href="https://link.segmentfault.com/?enc=z%2FDk88qEEYS%2BqKWC8cFqdA%3D%3D.wlDwDWVV%2BV0L9NAPQ26fz7cD7zd2Sd9bw6StYyoH0M0%3D" rel="nofollow">Promise/A+</a>中要求的,其次是业务场景的需求,当我们碰到那种<code>Promise</code>的<code>resolve</code>里的<code>Promise</code>的<code>resolve</code>里又包了一个<code>Promise</code>的话,就需要递归取值,直到<code>x</code>不是<code>Promise</code>对象。</li>
</ul>
<h2>4、catch</h2>
<pre><code class="js">//catch方法
catch(onRejected){
return this.then(null,onRejected)
}</code></pre>
<h2>5、finally</h2>
<p><code>finally</code>方法用于指定不管 <code>Promise</code> 对象最后状态如何,都会执行的操作。该方法是 <code>ES2018</code> 引入标准的。</p>
<pre><code class="js">finally(fn) {
return this.then(value => {
fn();
return value;
}, reason => {
fn();
throw reason;
});
};</code></pre>
<h2>6、resolve/reject</h2>
<p>大家一定都看到过<code>Promise.resolve()</code>、<code>Promise.reject()</code>这两种用法,它们的作用其实就是返回一个Promise对象,我们来实现一下。</p>
<pre><code class="js">static resolve(val) {
return new MyPromise((resolve, reject) => {
resolve(val)
})
}
//reject方法
static reject(val) {
return new MyPromise((resolve, reject) => {
reject(val)
})
}</code></pre>
<h2>7、all</h2>
<p><code>all</code>方法可以说是<code>Promise</code>中很常用的方法了,它的作用就是将一个数组的<code>Promise</code>对象放在其中,当全部<code>resolve</code>的时候就会执行<code>then</code>方法,当有一个<code>reject</code>的时候就会执行<code>catch</code>,并且他们的结果也是按着数组中的顺序来排放的,那么我们来实现一下。</p>
<pre><code class="js">static all(promiseArr) {
return new MyPromise((resolve, reject) => {
let result = [];
promiseArr.forEach((promise, index) => {
promise.then((value) => {
result[index] = value;
if (result.length === promiseArr.length) {
resolve(result);
}
}, reject);
});
});
}</code></pre>
<h2>8、race</h2>
<p><code>race方</code>法虽然不常用,但是在<code>Promise</code>方法中也是一个能用得上的方法,它的作用是将一个<code>Promise</code>数组放入<code>race</code>中,哪个先执行完,<code>race</code>就直接执行完,并从<code>then</code>中取值。我们来实现一下吧。</p>
<pre><code class="js">static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(promise => {
promise.then((value) => {
resolve(value);
}, reject);
});
});
}</code></pre>
<h2>9、deferred</h2>
<pre><code class="js">static deferred() {
let dfd = {};
dfd.promies = new MyPromise((resolve, reject) => {
dfd.resolve = resolve;
dfd.rfeject = reject;
});
return dfd;
};</code></pre>
<p>什么作用呢?看下面代码你就知道了</p>
<pre><code class="js">
let fs = require('fs')
let MyPromise = require('./MyPromise')
//Promise上的语法糖,为了防止嵌套,方便调用
//坏处 错误处理不方便
function read(){
let defer = MyPromise.defer()
fs.readFile('./1.txt','utf8',(err,data)=>{
if(err)defer.reject(err)
defer.resolve(data)
})
return defer.Promise
}
</code></pre>
<h2>10、测试</h2>
<pre><code class="js">const mp1 = MyPromise.resolve(1);
const mp2 = MyPromise.resolve(2);
const mp3 = MyPromise.resolve(3);
const mp4 = MyPromise.reject(4);
MyPromise.all([mp1, mp2, mp3]).then(x => {
console.log(x);
}, (err) => {
console.log('err1', err);
})
MyPromise.race([mp1, mp4, mp2, mp3]).then(x => {
console.log(x);
}, (err) => {
console.log('err2', err);
})
var mp = new MyPromise((resolve, reject) => {
console.log(11111);
setTimeout(() => {
resolve(22222);
console.log(3333);
}, 1000);
});
mp.then(x => {
console.log(x);
}, (err) => {
console.log('err2', err);
})
//11111
//[ 1, 2, 3 ]
//1
//3333
//22222</code></pre>
<p><a href="https://link.segmentfault.com/?enc=fjq7dq7SFhBYff5pofkO%2Fw%3D%3D.SXTmgbyCvLq7vOR%2F8yLe4N7B%2F7fLEdyLu3U8g84KvCcHvlRc2aSFk5X8OCU7n67rjVtqa2z8ZQi%2BG7pK%2BeEhI2ezyRiza2SDjLhfjFyOOb4%3D" rel="nofollow">完整源码请查看</a></p>
<p>如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 <a href="https://link.segmentfault.com/?enc=26glUOnlt74X556zkzte6Q%3D%3D.yV8zqHoFz2it5Cce1FSxCccR45VmniUKY5i610Rlfa0ZYn9FkpPXCxTjtpRodPAa" rel="nofollow">star</a>,对作者也是一种鼓励。</p>
Flex常见布局实例
https://segmentfault.com/a/1190000018665605
2019-03-26T22:06:06+08:00
2019-03-26T22:06:06+08:00
Alan
https://segmentfault.com/u/xc_xiang
0
<blockquote>如果对flex不是很熟悉的同学,可以看一下我的另一篇文章<a href="https://segmentfault.com/a/1190000018663998">Flex 布局</a>
</blockquote>
<h2>1、网格布局</h2>
<h3>1.1、基本网格布局</h3>
<p>最简单的网格布局,就是平均分布。<br>HTML代码如下。</p>
<pre><code class="html"><div class="Grid">
<div class="Grid-cell">1/2</div>
<div class="Grid-cell">1/2</div>
</div>
<div class="Grid">
<div class="Grid-cell">1/3</div>
<div class="Grid-cell">1/3</div>
<div class="Grid-cell">1/3</div>
</div></code></pre>
<p>CSS代码如下。</p>
<pre><code class="css">.Grid {
display: flex;
}
.Grid-cell {
flex: 1;
background: #eee;
margin: 10px;
}</code></pre>
<p>这里最关键的就是<code>flex:1</code>使得各个子元素可以等比伸缩<br><img src="/img/bVbqtCi?w=823&h=91" alt="图片描述" title="图片描述"></p>
<h3>1.2、百分比布局</h3>
<p>某个网格的宽度为固定的百分比,其余网格平均分配剩余的空间。<br>HTML代码如下。</p>
<pre><code class="html"><div class="Grid">
<div class="Grid-cell col2">50%</div>
<div class="Grid-cell">auto</div>
<div class="Grid-cell ">auto</div>
</div>
<div class="Grid">
<div class="Grid-cell">auto</div>
<div class="Grid-cell col2">50%</div>
<div class="Grid-cell clo3">1/3</div>
</div></code></pre>
<p>CSS代码如下。</p>
<pre><code class="css">.col2 {
flex: 0 0 50%;
}
.col3 {
flex: 0 0 33.3%;
}</code></pre>
<p>这里最关键的是通过<code>flex</code>的第三个属性,也就是<code>flex-basis</code>来定义元素占据的空间。<br><img src="/img/bVbqtDL?w=815&h=84" alt="图片描述" title="图片描述"></p>
<h2>2、圣杯布局</h2>
<p><strong>圣杯布局</strong>(Holy Grail Layout:)指的是一种最常见的网站布局。页面从上到下,分成三个部分:头部(header),躯干(body),尾部(footer)。其中躯干又水平分成三栏,从左到右为:导航、主栏、副栏。<br>HTML代码如下。</p>
<pre><code class="html"><div class="container">
<header class="bg">header</header>
<div class="body">
<main class="content bg">content</main>
<nav class="nav bg">nav</nav>
<aside class="ads bg">aside</aside>
</div>
<footer class="bg">footer</footer>
</div></code></pre>
<p>CSS代码如下。</p>
<pre><code class="css">.container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.body {
display: flex;
flex: 1;
}
header,
footer {
flex: 0 0 100px;
}
.content {
flex: 1;
}
.ads,
.nav {
flex: 0 0 100px;
}
.nav {
order: -1;
}
.bg {
background: #eee;
margin: 10px;
}
@media (max-width: 768px) {
.body {
flex-direction: column;
flex: 1;
}
.nav,
.ads,
.content {
flex: auto;
}
}</code></pre>
<p>这里面需要注意的点有</p>
<ul>
<li>
<code>container</code>的<code>flex-direction: column</code>这样保证了<code>header</code>,<code>body</code>,<code>footer</code>是在垂直轴上排列</li>
<li>
<code>header</code>,<code>footer</code>的高度可以通过<code>flex: 0 0 100px</code>来控制</li>
<li>
<code>nav</code>可以通过<code>order:-1</code>来调整位置</li>
<li>通过<code>@media (max-width: 768px)</code>来调整小屏幕的页面结构</li>
</ul>
<p><img src="/img/bVbqtLI?w=746&h=602" alt="图片描述" title="图片描述"></p>
<h2>3、侧边固定宽度</h2>
<p>侧边固定宽度,右边自适应<br>html代码如下。</p>
<pre><code class="html"><div class="container1">
<div class="aside1 bg">aside</div>
<div class="body1">
<div class="header1 bg">header</div>
<div class="content1 bg">content</div>
<div class="footer1 bg">footer</div>
</div>
</div></code></pre>
<p>CSS代码如下。</p>
<pre><code class="css">.bg {
background: #eee;
margin: 10px;
}
.container1 {
min-height: 100vh;
display: flex;
}
.aside1 {
/* flex: 0 0 200px; */
flex: 0 0 20%;
}
.body1 {
display: flex;
flex-direction: column;
flex: 1;
}
.content1 {
flex: 1;
}
.header1, .footer1 {
flex: 0 0 10%;
}</code></pre>
<p>这个和上面的基本差不多就不做解释了。<br><img src="/img/bVbqtRd?w=892&h=721" alt="图片描述" title="图片描述"></p>
<h2>4、流式布局</h2>
<p>每行的项目数固定,会自动分行。<br>html代码如下</p>
<pre><code class="html"><div class="container2">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div></code></pre>
<p>css代码如下</p>
<pre><code class="css">.container2 {
width: 200px;
height: 150px;
display: flex;
flex-flow: row wrap;
align-content: flex-start;
}
.item {
box-sizing: border-box;
background: #eee;
flex: 0 0 20%;
height: 40px;
margin: 5px;
}</code></pre>
<p>这里主要使用到了<code>flex-flow: row wrap</code>使得子元素水平排列,并且换行<br><img src="/img/bVbqtTq?w=210&h=193" alt="图片描述" title="图片描述"></p>
<h2>总结</h2>
<p>这是我简单总结的一些布局。如有错误,欢迎指正。<a href="https://link.segmentfault.com/?enc=HgT%2BjmO0Uo3vxhDDbPcbaw%3D%3D.c9tZBp7h1LLHsUfiiCH4x9JUvWpFTiAduEO5nh6vKAq9xOyNqOJwCt3vFAzf%2BRId" rel="nofollow">更多系列文章</a></p>
Flex 布局
https://segmentfault.com/a/1190000018663998
2019-03-26T19:38:18+08:00
2019-03-26T19:38:18+08:00
Alan
https://segmentfault.com/u/xc_xiang
2
<blockquote>在没有接触Flex之前一直使用float、display、position 。说实话用起来非常恶心。当使用Flex时,我们可以简洁优雅实现复杂的页面布局</blockquote>
<h2>1、Flex 布局?<img src="/img/remote/1460000018664133" alt="flex" title="flex">
</h2>
<p>在 flex 容器中默认存在两条轴,水平主轴(main axis) 和垂直的交叉轴(cross axis),这是默认的设置,当然你可以通过修改使垂直方向变为主轴,水平方向变为交叉轴。</p>
<p>首先,实现 flex 布局需要先指定一个容器,任何一个容器都可以被指定为 flex 布局,这样容器内部的元素就可以使用 flex 来进行布局。</p>
<pre><code class="css">.container {
display: flex | inline-flex; //可以有两种取值
}</code></pre>
<p>分别生成一个块状或行内的 flex 容器盒子。简单说来,如果你使用块元素如 div,你就可以使用 flex,而如果你使用行内元素,你可以使用 inline-flex。</p>
<blockquote>
<strong>注意</strong>:当时设置 flex 布局之后,子元素的 float、clear、vertical-align 的属性将会失效。</blockquote>
<h3>1.1、容器的属性</h3>
<p>容器有以下6个属性</p>
<ul>
<li>flex-direction</li>
<li>flex-wrap</li>
<li>flex-flow</li>
<li>justify-content</li>
<li>align-items</li>
<li>align-content</li>
</ul>
<h4>1.1.1 、flex-direction属性</h4>
<p>flex-direction属性决定主轴的方向(即项目的排列方向)。</p>
<pre><code class="css">.box {
flex-direction: row | row-reverse | column | column-reverse;
}</code></pre>
<p>默认值:row,主轴为水平方向,起点在左端。<br>row-reverse:主轴为水平方向,起点在右端。<br><img src="/img/remote/1460000018664134?w=822&h=217" alt="在这里插入图片描述" title="在这里插入图片描述"><br>column:主轴为垂直方向,起点在上沿;<br>column-reverse:主轴为垂直方向,起点在下沿;<br><img src="/img/remote/1460000018664135?w=619&h=606" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.1.2、flex-wrap: 决定容器内项目是否可换行</h4>
<p>默认情况下,项目都排在主轴线上不换行,但使用 flex-wrap 可实现项目的换行。</p>
<pre><code class="css">.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}</code></pre>
<ul>
<li>nowrap (默认值)不换行,即当主轴尺寸固定时,当空间不足时,项目尺寸会随之调整而并不会挤到下一行。</li>
<li>wrap:项目主轴总尺寸超出容器时换行,第一行在上方</li>
<li>wrap-reverse:换行,第一行在下方</li>
</ul>
<p><img src="/img/remote/1460000018664136?w=726&h=639" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.1.3、flex-flow: flex-direction 和 flex-wrap 的简写形式</h4>
<pre><code class="css">.container {
flex-flow: <flex-direction> || <flex-wrap>;
}</code></pre>
<p>默认值为: row nowrap;</p>
<h4>1.1.4、justify-content:定义了项目在主轴的对齐方式。</h4>
<pre><code class="css">.container {
justify-content: flex-start | flex-end | center | space-between | space-around;
}</code></pre>
<ul>
<li>flex-start(默认值):左对齐</li>
<li>flex-end:右对齐</li>
<li>center:居中</li>
<li>space-between:两端对齐,项目之间的间隔相等,即剩余空间等分成间隙。</li>
<li>space-around:每个项目两侧的间隔相等,所以项目之间的间隔比项目与边缘的间隔大一倍。</li>
</ul>
<p><img src="/img/remote/1460000018664137" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.1.5、align-items: 定义了项目在交叉轴上的对齐方式</h4>
<pre><code class="css">.container {
align-items: flex-start | flex-end | center | baseline | stretch;
}</code></pre>
<p>建立在主轴为水平方向时测试,即 <code>flex-direction: row</code><br>默认值为 <code>stretch</code> 即如果项目未设置高度或者设为 <code>auto</code>,将占满整个容器的高度。假设容器高度设置为 100px,而项目都没有设置高度的情况下,则项目的高度也为 100px。<br><img src="/img/remote/1460000018664138" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>flex-start</code>:交叉轴的起点对齐,假设容器高度设置为 100px,而项目分别为 30px,60px, 100px, 则如上图显示。<br><img src="/img/remote/1460000018664139?w=394&h=332" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>flex-end</code>:交叉轴的终点对齐<br><img src="/img/remote/1460000018664140" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>center</code>:交叉轴的中点对齐<br><img src="/img/remote/1460000018664141" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>baseline</code>: 项目的第一行文字的基线对齐<br><img src="/img/remote/1460000018664142?w=330&h=322" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.1.6、align-content: 定义了多根轴线的对齐方式,如果项目只有一根轴线,那么该属性将不起作用</h4>
<pre><code class="css">.container {
align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}</code></pre>
<p>当我们 <code>flex-wrap</code> 设置为 <code>nowrap</code> 的时候,容器仅存在一根轴线,因为项目不会换行,就不会产生多条轴线。<br>当我们 <code>flex-wrap</code> 设置为 <code>wrap</code> 的时候,容器可能会出现多条轴线,这时候就需要去设置多条轴线之间的对齐方式了。</p>
<p>建立在主轴为水平方向时测试,即 <code>flex-direction: row</code>,<code>flex-wrap: wrap</code>;<br>默认值为 <code>stretch</code>平分容器的垂直方向上的空间。如果没有设置高度将会撑开整个容器。<br><img src="/img/remote/1460000018664143?w=240&h=334" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>flex-start</code>:轴线全部在交叉轴上的起点对齐<br><img src="/img/remote/1460000018664144" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>flex-end</code>:轴线全部在交叉轴上的终点对齐<br><img src="/img/remote/1460000018664145" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>center</code>:轴线全部在交叉轴上的中间对齐<br><img src="/img/remote/1460000018664146" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>space-between</code>:轴线两端对齐,之间的间隔相等,即剩余空间等分成间隙。<br><img src="/img/remote/1460000018664147?w=215&h=334" alt="在这里插入图片描述" title="在这里插入图片描述"><br><code>space-around</code>:每个轴线两侧的间隔相等,所以轴线之间的间隔比轴线与边缘的间隔大一倍。<br><img src="/img/remote/1460000018664148?w=205&h=324" alt="在这里插入图片描述" title="在这里插入图片描述"><br>到这里关于容器上的所有属性都讲完了,接下来就来讲讲关于在 flex item 上的属性。</p>
<h3>1.2、Flex项目的属性</h3>
<p>主要有以下6个属性设置在项目上。</p>
<ul>
<li>order</li>
<li>flex-grow</li>
<li>flex-shrink</li>
<li>flex-basis</li>
<li>flex</li>
<li>align-self</li>
</ul>
<h4>1.2.1、order属性</h4>
<p><code>order</code>属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。</p>
<pre><code class="css">.item {
order: <integer>;
}</code></pre>
<p><img src="/img/remote/1460000018664149" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.2.2、 flex-grow: 定义项目的放大比例</h4>
<pre><code class="css">.item {
flex-grow: <number>;
}</code></pre>
<p>如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。<br><img src="/img/remote/1460000018664150?w=599&h=80" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h4>1.2.3、flex-shrink属性:定义了项目的缩小比例</h4>
<pre><code class="css">.item {
flex-shrink: <number>;
}</code></pre>
<p>默认值: 1,即如果空间不足,该项目将缩小,负值对该属性无效。<br><img src="/img/remote/1460000018664151" alt="在这里插入图片描述" title="在这里插入图片描述"><br>这里可以看出,虽然每个项目都设置了宽度为 50px,但是由于自身容器宽度只有 200px,这时候每个项目会被同比例进行缩小,因为默认值为 1。</p>
<blockquote>
<strong>注意</strong>:如果所有项目的 flex-shrink 属性都为 1,当空间不足时,都将等比例缩小。 <br>如果一个项目的 flex-shrink 属性为 0,其他项目都为 1,则空间不足时,前者不缩小。</blockquote>
<h4>1.2.4、 flex-basis属性:定义了在分配多余空间之前,项目占据的主轴空间,浏览器根据这个属性,计算主轴是否有多余空间</h4>
<p><code>flex-basis</code>属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。</p>
<pre><code class="css">.item {
flex-basis: <length> | auto; /* default auto */
}</code></pre>
<p>默认值:auto,即项目本来的大小, 这时候 item 的宽高取决于 width 或 height 的值。<br><strong>当主轴为水平方向的时候,当设置了 <code>flex-basis</code>,项目的宽度设置值会失效,<code>flex-basis</code> 需要跟 <code>flex-grow</code> 和 <code>flex-shrink </code>配合使用才能发挥效果。</strong></p>
<ul>
<li>当 <code>flex-basis </code>值为 <code>0 % </code>时,是把该项目视为零尺寸的,故即使声明该尺寸为 140px,也并没有什么用。</li>
<li>当 <code>flex-basis</code> 值为 auto 时,则跟根据尺寸的设定值(假如为 100px),则这 100px 不会纳入剩余空间。</li>
</ul>
<h4>1.2.5、flex属性</h4>
<p><code>flex</code>属性是<code>flex-grow</code>, <code>flex-shrink</code> 和 <code>flex-basis</code>的简写,默认值为<code>0 1 auto</code>。后两个属性可选。</p>
<pre><code class="css">.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}</code></pre>
<p>该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。</p>
<p>建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。<br>关于 flex 取值,还有许多特殊的情况,可以按以下来进行划分:</p>
<ul><li>当 flex 取值为一个非负数字,则该数字为 flex-grow 值,flex-shrink 取 1,flex-basis 取 0%,如下是等同的:</li></ul>
<pre><code class="css">.item {flex: 1;}
.item {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}</code></pre>
<ul><li>当 flex 取值为一个长度或百分比,则视为 flex-basis 值,flex-grow 取 1,flex-shrink 取 1,有如下等同情况(注意 0% 是一个百分比而不是一个非负数字)</li></ul>
<pre><code class="css">.item-1 {flex: 0%;}
.item-1 {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}
.item-2 {flex: 24px;}
.item-2 {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 24px;
}</code></pre>
<ul><li>当 flex 取值为两个非负数字,则分别视为 flex-grow 和 flex-shrink 的值,flex-basis 取 0%,如下是等同的:</li></ul>
<pre><code class="css">.item {flex: 2 3;}
.item {
flex-grow: 2;
flex-shrink: 3;
flex-basis: 0%;
}</code></pre>
<h4>1.2.6、align-self属性:允许单个项目有与其他项目不一样的对齐方式</h4>
<p>单个项目覆盖 父元素的<code>align-items</code> 定义的属性<br>默认值为<code> auto</code>,表示继承父元素的 <code>align-items </code>属性,如果没有父元素,则等同于 stretch。</p>
<pre><code class="css">.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}</code></pre>
<p>这个跟 <code>align-items</code> 属性时一样的,只不过 <code>align-self </code>是对单个项目生效的,而 <code>align-items</code> 则是对容器下的所有项目生效的。<br><img src="/img/remote/1460000018664152?w=310&h=307" alt="在这里插入图片描述" title="在这里插入图片描述"><br>容器 <code>align-items</code> 设置为 <code>flex-start</code>,而第三个子元素的<code>align-self: flex-end</code>;</p>
<p><a href="https://link.segmentfault.com/?enc=FpHJ%2FTH%2BXW%2Fk7WGDHcKv%2FA%3D%3D.G59HoT2x9TDJ3FS%2Bvo85O%2FSxep0Ojn5i3Ro0yalAiiFV%2F4lTJo5gL%2FnuME04FoUW" rel="nofollow">更多系列文章请看</a></p>
ES6学习(三)之Set的模拟实现
https://segmentfault.com/a/1190000018633904
2019-03-24T18:55:17+08:00
2019-03-24T18:55:17+08:00
Alan
https://segmentfault.com/u/xc_xiang
1
<blockquote><a href="https://link.segmentfault.com/?enc=yNfl0%2F9OrrbMrDK8wolD1g%3D%3D.rpYkNwW6yem%2BIE%2BOQd1w4qVXdH0voQvXiUHrZqrgbKfkgsH3S%2FY6sVXNCGut%2F0iL" rel="nofollow">更多系列文章请看</a></blockquote>
<p>在实现之前我们可以通过<a href="https://link.segmentfault.com/?enc=28CxiP%2BkDqbSVetHk6zwag%3D%3D.QMiFkzPNITlf4GYZVnz5hY7YsyvzjmWpQlqzIAFY6afDv29Tm9YfF1OG1W%2FqrH60" rel="nofollow">阮一峰的ECMAScript 6 入门</a>了解一下Set的基本信息</p>
<h2>1、Set的基本语法</h2>
<pre><code class="js">new Set([ iterable ])</code></pre>
<p>可以传递一个可迭代对象,它的所有元素将被添加到新的 <code>Set</code>中。如果不指定此参数或其值为<code>null</code>,则新的 <code>Set</code>为空。</p>
<pre><code class="js">let s = new Set([ 1, 2, 3 ]) // Set(3) {1, 2, 3}
let s2 = new Set() // Set(0) {}
let s3 = new Set(null /* or undefined */) // Set(0) {}</code></pre>
<h3>1.1 实例属性和方法</h3>
<p><strong>属性</strong></p>
<ul>
<li>
<code>constructor</code>: <code>Set</code>的构造函数</li>
<li>
<code>size</code>: <code>Set </code>长度</li>
</ul>
<p><strong>操作方法</strong></p>
<ul>
<li>
<code>add(value)</code>:在Set对象尾部添加一个元素。返回该Set对象。</li>
<li>
<code>has(value)</code>:返回一个布尔值,表示该值在Set中存在与否。</li>
<li>
<code>delete(value)</code>:移除Set中与这个值相等的元素,返回<code>has(value)</code>在这个操作前会返回的值(即如果该元素存在,返回true,否则返回false)</li>
</ul>
<p>-<code>clear()</code>:移除Set对象内的所有元素。没有返回值</p>
<p><strong>遍历方法</strong></p>
<ul><li>
<code>keys()</code>:返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。</li></ul>
<p>-<code>values()</code>:返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。</p>
<ul><li>
<code>entries()</code>:返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值的[value, value]数组。为了使这个方法和Map对象保持相似, 每个值的键和值相等。</li></ul>
<p>-<code>forEach(callbackFn[, thisArg])</code>:按照插入顺序,为Set对象中的每一个值调用一次callBackFn。如果提供了thisArg参数,回调中的this会是这个参数。</p>
<pre><code class="js">
let s = new Set()
s.add(1) // Set(1) {1}
.add(2) // Set(2) {1, 2}
.add(NaN) // Set(2) {1, 2, NaN}
.add(NaN) // Set(2) {1, 2, NaN}
// 注意这里因为添加完元素之后返回的是该Set对象,所以可以链式调用
// NaN === NaN 结果是false,但是Set中只会存一个NaN
s.has(1) // true
s.has(NaN) // true
s.size // 3
s.delete(1)
s.has(1) // false
s.size // 2
s.clear()
s // Set(0) {}
let s2 = new Set([ 's', 'e', 't' ])
s2 // SetIterator {"s", "e", "t"}
s2.keys() // SetIterator {"s", "e", "t"}
s2.values() // SetIterator {"s", "e", "t"}
s2.entries() // SetIterator {"s", "e", "t"}
// log
[ ...s2 ] // ["s", "e", "t"]
[ ...s2.keys() ] // ["s", "e", "t"]
[ ...s2.values() ] // ["s", "e", "t"]
[ ...s2.entries() ] // [["s", "s"], ["e", "e"], ["t", "t"]]
s2.forEach(function (value, key, set) {
console.log(value, key, set, this)
})
// s s Set(3) {"s", "e", "t"} Window
// e e Set(3) {"s", "e", "t"} Window
// t t Set(3) {"s", "e", "t"} Window
s2.forEach(function () {
console.log(this)
}, { name: 'qianlongo' })
// {name: "qianlongo"}
// {name: "qianlongo"}
// {name: "qianlongo"}
for (let value of s2) {
console.log(value)
}
// s
// e
// t
for (let value of s2.entries()) {
console.log(value)
}
// ["s", "s"]
// ["e", "e"]
// ["t", "t"]</code></pre>
<h2>2、模拟实现</h2>
<h2>2.1、Set的整体结构</h2>
<pre><code class="js">
class Set {
constructor (iterable) {}
get size () {}
has () {}
add () {}
delete () {}
clear () {}
forEach () {}
keys () {}
values () {}
entries () {}
[ Symbol.iterator ] () {}
}</code></pre>
<p>除此之外我们还需要二个辅助方法<br>1、<code>forOf</code>,模拟<code>for of</code>行为, 对迭代器对象进行遍历操作。</p>
<pre><code class="js">const forOf = (iterable, callback, ctx) => {
let result
iterable = iterable[ Symbol.iterator ]()
result = iterable.next()
while (!result.done) {
callback.call(ctx, result.value)
result = iterable.next()
}
}</code></pre>
<p>2、<code>Iterator</code>迭代器,更多迭代器信息请看<a href="https://link.segmentfault.com/?enc=R%2BIshX3SB%2FvJ%2BYerlVkFMg%3D%3D.1Z8U5XZXalkahbiRPqnBiZYZC1T4QzRkqzAavhC47gHGNju6aW%2FlMXCFRvNxOEWi" rel="nofollow">Iterator</a>,我们这里面用迭代器是为了让我们的set的<code>values()</code>等可进行遍历。</p>
<pre><code class="js">class Iterator {
constructor (arrayLike, iteratee = (value) => value) {
this.value = Array.from(arrayLike)
this.nextIndex = 0
this.len = this.value.length
this.iteratee = iteratee
}
next () {
let done = this.nextIndex >= this.len
let value = done ? undefined : this.iteratee(this.value[ this.nextIndex++ ])
return { done, value }
}
[ Symbol.iterator ] () {
return this
}
}</code></pre>
<h3>2.3、实现源码</h3>
<pre><code class="js">class Set {
constructor(iterable){
this.value = [];
if(!this instanceof Set) throw new Error('Constructor Set requires "new"');
if(isDef(iterable)) {
if(typeof iterable[ Symbol.iterator ] !== 'function') new Error(`${iterable} is not iterable`);
// 循环可迭代对象,初始化
forOf(iterable, value => this.add(value));
}
}
get size(){
return this.value.length;
}
has(val) {
return this.value.includes(val); // [ NaN ].includes(NaN)会返回true,正好Set也只能存一个NaN
}
add(val) {
if(!this.has(val)) {
this.value.push(val);
}
return this;
}
delete(val) {
const index = this.value.indexOf(val);
if (index > -1) {
this.value.splice(index, 1);
return true;
}
return false;
}
clear() {
this.value.length = 0;
}
forEach(cb, arg) {
forOf(this.values(), val => {
cb.call(arg, val, val, this);
})
}
keys() {
return new Iterator(this.value);
}
values() {
return this.keys();
}
entries() {
return new Iterator(this.value, (value) => [ value, value ])
}
[Symbol.iterable]() {
return this.values();
}
}</code></pre>
<blockquote>模拟过程中可能会有相应的错误,也不是和原生的实现完全一致。仅当学习之用.</blockquote>
ES6学习(二)之解构赋值及其原理
https://segmentfault.com/a/1190000018628030
2019-03-23T21:42:43+08:00
2019-03-23T21:42:43+08:00
Alan
https://segmentfault.com/u/xc_xiang
3
<blockquote><a href="https://link.segmentfault.com/?enc=QBovY3Cws54Yw9e0k9wSlA%3D%3D.JeyJub8%2FC9UbY9n8ttAUo8fkuBq5r4pWvRLh%2BP4bMPzepGhZdttmk%2BNxH6JeFHhv" rel="nofollow">更多系列文章请看</a></blockquote>
<h2>1、基本语法</h2>
<h3>1.1、数组</h3>
<pre><code class="js">// 基础类型解构
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1, 2, 3
// 对象数组解构
let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}]
console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'}
// ...解构
let [head, ...tail] = [1, 2, 3, 4]
console.log(head, tail) // 1, [2, 3, 4]
// 嵌套解构
let [a, [b], d] = [1, [2, 3], 4]
console.log(a, b, d) // 1, 2, 4
// 解构不成功为undefined
let [a, b, c] = [1]
console.log(a, b, c) // 1, undefined, undefined
// 解构默认赋值
let [x = 1] = [undefined];// x=1;
let [x = 1] = [null];// x=null; // 数组成员严格等于undefined,默认值才会生效
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined 因为x用y做默认值时,y还没有声明</code></pre>
<h3>1.2 对象</h3>
<pre><code class="js">// 对象属性解构
let { f1, f2 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2
// 可以不按照顺序,这是数组解构和对象解构的区别之一
let { f2, f1 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2
// 解构对象重命名
let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' }
console.log(rename, f2) // test1, test2
// 嵌套解构
let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } }
console.log(f11) // test11
// 默认值
let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'}
console.log(f1, rename) // current1, current2</code></pre>
<h3>1.3 字符串/数值/布尔值</h3>
<pre><code class="js">// String
let [ a, b, c, ...rest ] = 'test123'
console.log(a, b, c, rest) // t, e, s, [ 't', '1', '2', '3' ]
let {length : len} = 'hello'; // en // 5
// number
let {toString: s} = 123;
s === Number.prototype.toString // true
// boolean
let {toString: s} = true;
s === Boolean.prototype.toString // true
// Map
let [a, b] = new Map().set('f1', 'test1').set('f2', 'test2')
console.log(a, b) // [ 'f1', 'test1' ], [ 'f2', 'test2' ]
// Set
let [a, b] = new Set([1, 2, 3])
console.log(a, b) // 1, 2</code></pre>
<h2>2、使用场景</h2>
<h3>2.1、 浅拷贝</h3>
<pre><code class="js">let colors = [ "red", "green", "blue" ];
let [ ...clonedColors ] = colors;
console.log(clonedColors); // "[red,green,blue]"</code></pre>
<blockquote>注意这里是浅拷贝</blockquote>
<h3>2.2、 交换变量</h3>
<pre><code class="js">let x = 1;
let y = 2;
[x, y] = [y, x];</code></pre>
<h3>2.3、遍历Map结构</h3>
<pre><code class="js">var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}</code></pre>
<h3>2.4、函数参数</h3>
<pre><code class="js">function add(a,b,{c,d}){
// ...
}
add(1,2,{});
add(1,2)//Uncaught TypeError: Cannot destructure property `c` of 'undefined' or 'null'</code></pre>
<blockquote>解构赋值的规则是,若等号右边的值不是对象或者数组,就会先将其转化成对象。由于undefined和null无法转化成对象,所以对其进行解构赋值时会报错。</blockquote>
<p>它实际上是这样运行的:</p>
<pre><code class="js">function add(a, b, options) {
let { c,d } = options;
// ...
}</code></pre>
<p>由于<code>add(1,2)</code>没有传<code>options</code>导致异常,如果我们options是选填的那么可以像下面这样</p>
<pre><code class="js">function add(a,b,{c,d}={}){
// ...
}</code></pre>
<p>当然大部分情况我们可以给默认值</p>
<pre><code class="js">function move ({x:0,y:0}={}){}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]</code></pre>
<pre><code class="js">function move ({x,y}={x:0,y:0}){}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]</code></pre>
<p>上面代码是为函数<code>move</code>的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。<br><code>undefined</code>就会触发函数参数的默认值。</p>
<pre><code class="js">[1, undefined, 3].map((x = 'yes') => x);</code></pre>
<h2>3、基本原理</h2>
<p>解构是<code>ES6</code>提供的语法糖,其实内在是针对可迭代对象的<code>Iterator</code>接口,通过遍历器按顺序获取对应的值进行赋值。这里需要提前懂得<code>ES6</code>的两个概念:</p>
<ul>
<li>Iterator</li>
<li>可迭代对象</li>
</ul>
<h3>3.1、Iterator概念</h3>
<p><code>Iterato</code>r是一种接口,为各种不一样的数据解构提供统一的访问机制。任何数据解构只要有<code>Iterator</code>接口,就能通过遍历操作,依次按顺序处理数据结构内所有成员。ES6中的for of的语法相当于遍历器,会在遍历数据结构时,自动寻找<code>Iterator</code>接口。<br>Iterator作用:</p>
<ul>
<li>为各种数据解构提供统一的访问接口</li>
<li>使得数据解构能按次序排列处理</li>
<li>可以使用ES6最新命令 for of进行遍历</li>
</ul>
<pre><code class="js">function makeIterator(array) {
var nextIndex = 0
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true}
}
};
}
var it = makeIterator([0, 1, 2])
console.log(it.next().value) // 0
console.log(it.next().value) // 1
console.log(it.next().value) // 2</code></pre>
<h3>3.2、可迭代对象</h3>
<p>可迭代对象是<code>Iterator</code>接口的实现。这是<code>ECMAScript 2015</code>的补充,它不是内置或语法,而仅仅是协议。任何遵循该协议点对象都能成为可迭代对象。可迭代对象得有两个协议:<strong>可迭代协议</strong>和<strong>迭代器协议</strong>。</p>
<p><code>可迭代协议</code>:对象必须实现<code>@@iterator</code>方法。即对象或其原型链上必须有一个名叫<code>Symbol.iterator</code>的属性。该属性的值为无参函数,函数返回迭代器协议。</p>
<table>
<thead><tr>
<th>属性</th>
<th>值</th>
</tr></thead>
<tbody><tr>
<td><code>Symbol.iterator</code></td>
<td>返回一个对象的无参函数,被返回对象符合迭代器协议。</td>
</tr></tbody>
</table>
<p><code>迭代器协议</code>:定义了标准的方式来产生一个有限或无限序列值。其要求必须实现一个<code>next()</code>方法,该方法返回对象有<code>done(boolean)</code>和<code>value</code>属性。</p>
<table>
<thead><tr>
<th>属性</th>
<th align="left">值</th>
</tr></thead>
<tbody><tr>
<td><code>next</code></td>
<td align="left">返回一个对象的无参函数,被返回对象拥有两个属性:<code>done</code>和<code>value</code><br><code>done</code> :如果迭代器已经经过了被迭代序列时为 true。这时 value 可能描述了该迭代器的返回值。如果迭代器可以产生序列中的下一个值,则为 false。这等效于连同 done 属性也不指定。<br><code>value</code> :迭代器返回的任何 JavaScript 值。done 为 true 时可省略。</td>
</tr></tbody>
</table>
<p>通过以上可知,自定义数据结构,只要拥有<code>Iterator</code>接口,并将其部署到自己的<code>Symbol.iterator</code>属性上,就可以成为可迭代对象,能被<code>for of</code>循环遍历。</p>
<h3>3.3、解构语法糖</h3>
<p><code>String</code>、<code>Array</code>、<code>Map</code>、<code>Set</code>等原生数据结构都是可迭代对象,可以通过<code>for of</code>循环遍历它。故可以通过ES6解构语法糖依次获取对应的值。</p>
<pre><code class="js">// String
let str = 'test'
let iterFun = str[Symbol.iterator]
let iterator = str[Symbol.iterator]()
let first = iterator.next() // 等效于 let [first] = 'test'
console.log(iterFun, iterator, first)
// 打印
// [Function: [Symbol.iterator]], {}, { value: 't', done: false }
// Array
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
// 以下等效于 let [first, second, third, four] = ['a', 'b', 'c']
let first = iter.next() // { value: 'a', done: false }
let second = iter.next() // { value: 'b', done: false }
let third = iter.next() // { value: 'c', done: false }
let four = iter.next() // { value: undefined, done: true }
</code></pre>
<h2>4、性能</h2>
<p>当我们有解构赋值的形式来做函数参数时,执行的时候会增加很多中间变量,内存也会比之前高。但是业务代码还是更加关注可读性和可维护性。如果你写的是库代码,可以尝试这种优化,把参数展开后直接传递,到底能带来多少性能收益还得看最终的基准测试。</p>
<h4>参考</h4>
<p><a href="https://link.segmentfault.com/?enc=SykunGfG%2Bak%2FXiaVZJrL1w%3D%3D.pYFAMnJRm0jtojswoIQDCFFvrTAeoghyqcjLubOsPwDEqDDhxnjT%2FgHmppUi3wMH" rel="nofollow">ES6 的解构赋值前每次都创建一个对象吗?会加重 GC 的负担吗?</a></p>
ES6学习(一)之var、let、const
https://segmentfault.com/a/1190000018607632
2019-03-21T21:07:31+08:00
2019-03-21T21:07:31+08:00
Alan
https://segmentfault.com/u/xc_xiang
2
<blockquote><a href="https://link.segmentfault.com/?enc=ffF7oQYMsqxH2bi2bdfOpQ%3D%3D.Gk7sTK4J1caxVjYTdF6VTB3bb%2B98v1QwB%2FOOWVSbq%2F3L5ORvV%2BeluR66CT88p62x" rel="nofollow">更多前端文章</a></blockquote>
<h2>1、变量提升</h2>
<p>概述:变量可在声明之前使用。</p>
<pre><code class="js">console.log(a);//正常运行,控制台输出 undefined
var a = 1;
console.log(b);//报错,Uncaught ReferenceError: b is not defined
let b = 1;
console.log(c);//报错,Uncaught ReferenceError: c is not defined
const c = 1;</code></pre>
<p>var 命令经常会发生变量提升现象,按照一般逻辑,变量应该在声明之后使用才对。为了纠正这个现象,ES6 规定 let 和 const 命令不发生变量提升,使用 let 和 const 命令声明变量之前,该变量是不可用的。主要是为了减少运行时错误,防止变量声明前就使用这个变量,从而导致意料之外的行为。</p>
<h2>2、暂时性死区</h2>
<p>概述:如果在代码块中存在 let 或 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。</p>
<pre><code class="js">var tmp = 123;
if (true) {
tmp = 'abc';//报错,Uncaught ReferenceError: tmp is not defined
let tmp;
}</code></pre>
<p>剖析暂时性死区的原理,其实let/const同样也有提升的作用,但是和var的区别在于</p>
<ul>
<li>var在创建时就被初始化,并且赋值为undefined</li>
<li>let/const在进入块级作用域后,会因为提升的原因先创建,但不会被<strong>初始化</strong>,直到声明语句执行的时候才被初始化,初始化的时候如果使用let声明的变量没有赋值,则会默认赋值为undefined,而const必须在初始化的时候赋值。而创建到初始化之间的代码片段就形成了暂时性死区</li>
</ul>
<h2>3、不允许重复声明</h2>
<p><code>let</code>不允许在相同作用域内,重复声明同一个变量。</p>
<pre><code class="js">// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错</code></pre>
<h2>4、块级作用域</h2>
<p>在es5中我们会遇到下面这写情况<br>第一种场景,内层变量可能会覆盖外层变量。</p>
<pre><code class="js">var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';//没有块级作用域tmp变量提升到函数作用域里导致tmp为undefined
}
}
f(); // undefined</code></pre>
<p>第二种场景,用来计数的循环变量泄露为全局变量。</p>
<pre><code class="js">var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5</code></pre>
<p>上面代码中,变量<code>i</code>只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。</p>
<pre><code class="js"> let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}</code></pre>
<p>let实际上为 JavaScript 新增了块级作用域。</p>
<h2>5、const注意点</h2>
<p>1、const声明变量的时候必须赋值,否则会报错,同样使用const声明的变量被修改了也会报错<br>2、const声明变量不能改变,如果声明的是一个引用类型,则不能改变它的内存地址</p>
<pre><code class="js">const c ; //Uncaught SyntaxError: Missing initializer in const declaration
const a= {a:1};
a.a=2;
a={d:2};// Uncaught TypeError: Assignment to constant variable.</code></pre>
<blockquote>
<strong>本质</strong>:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了</blockquote>
<p>那么我们想得到不可修改的const要怎么做呢?应该使用<code>Object.freeze</code>方法。</p>
<pre><code class="js">const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
// 除了将对象本身冻结,对象的属性也应该冻结。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};</code></pre>
<p>参考文章<br><a href="https://link.segmentfault.com/?enc=G6KXpxMDDVWTOvQR3i965g%3D%3D.rf57xEr9VVwq%2FSwri1xUH1OICkJhPjRUT53SLjm5uMT%2FxPtcsnOvRK862oDNDPKx" rel="nofollow">ECMAScript 6 入门</a></p>
移动端适配
https://segmentfault.com/a/1190000018576842
2019-03-19T19:37:24+08:00
2019-03-19T19:37:24+08:00
Alan
https://segmentfault.com/u/xc_xiang
7
<blockquote>
<strong>前言</strong>:这周工作碰到了移动端1px的问题。以前一直写样式也没有特别注意着一点。还有就是rem的原理。这些其实就是比较常见的移动端适配问题。现阶段比较主流的适配方案有二种。一种是<code>flexible + rem</code>,另一种是<code>vw</code>
</blockquote>
<p>下面我们来看一下具体情况</p>
<h2>1、基本概念</h2>
<p>在了解具体方案原理前,我们先来看一下一些基本概念:</p>
<h3>1.1、物理像素(physical pixel)</h3>
<p>物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。</p>
<h3>1.2、设备独立像素(density-independent pixel)</h3>
<p>设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。(老早在没有 retina 屏之前,设备独立像素与物理像素是相等的)</p>
<h3>1.3、CSS像素</h3>
<p>CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。</p>
<h3>1.4、设备像素比(device pixel ratio)</h3>
<p>设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:</p>
<pre><code>设备像素比 = 物理像素 / 设备独立像素</code></pre>
<p>在<code>JavaScript</code>中,可以通过<code>window.devicePixelRatio</code>获取到当前设备的<code>dpr</code>。而在CSS中,可以通过<code>-webkit-device-pixel-ratio</code>,<code>-webkit-min-device-pixel-ratio</code>和 <code>-webkit-max-device-pixel-ratio</code>进行媒体查询,对不同<code>dpr</code>的设备,做一些样式适配(这里只针对webkit内核的浏览器和webview)<br>因此在iphone 6、7、8 的 dpr 为 2的设备中,一个设备独立像素便为 4 个物理像素,因此在 css 上设置的 1px 在其屏幕上占据的是 2个物理像素,0.5px 对应的才是其所能展示的最小单位。</p>
<h3>1.5、rem</h3>
<p>简单的理解,<code>rem</code>就是相对于根元素<code><html></code>的<code>font-size</code>来做计算。而我们的方案中使用<code>rem</code>单位,是能轻易的根据<code><html></code>的<code>ont-size</code>计算出元素的盒模型大小。而这个特色对我们来说是特别的有益处。</p>
<h2>2、flexible实现方案</h2>
<p>了解了前面一些相关概念之后,接下来我们来看实际解决方案。淘宝有一个名叫<a href="https://link.segmentfault.com/?enc=o7Dw0ACVQMpRSV%2F%2BN5%2BILQ%3D%3D.CpYobMhlfkZmlTft5d4JyvdrQNJDv7nAwtRv1zxHexa27AId5SzJqoelmO4HyqqL" rel="nofollow">lib-flexible</a>的库,而这个库就是用来解决H5页面终端适配的。<br>我们把屏幕分成10等分,那么</p>
<ul>
<li>物理像素为 750 = 375 * 2,那么 10rem = 750px,1rem = 75px;</li>
<li>物理像素为 1125 = 375 * 3,那么 10rem = 1125px,1rem = 112.5px ;</li>
<li>物理像素为 1242 = 414 * 3, 那么 10rem = 1242px,1rem = 124.2px;</li>
</ul>
<pre><code class="js">function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}</code></pre>
<h3>2.1、1px的物理像素的解决方案</h3>
<p>由此我们得到一个<strong>1px像素的解决方案</strong>。viewport 的 initial-scale 具有缩放页面的效果。对于 dpr=2 的屏幕,1px压缩一半便可与1px的设备像素比匹配,这就可以通过将缩放比 initial-scale 设置为 0.5=1/2 而实现。以此类推 dpr=3的屏幕可以将 initial-scale设置为 0.33=1/3 来实现。</p>
<pre><code class="js"><meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5"></code></pre>
<pre><code class="js">if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}</code></pre>
<h2>3、视口单位适配方案</h2>
<p>将视口宽度 window.innerWidth 和视口高度 window.innerHeight 等分为 100 份,且将这里的视口理解成 idealviewport 更为贴切,并不会随着 viewport 的不同设置而改变。</p>
<p>1、vw : 1vw 为视口宽度的 1%<br>2、vh : 1vh 为视口高度的 1%<br>3、vmin : vw 和 vh 中的较小值<br>4、vmax : 选取 vw 和 vh 中的较大值</p>
<p>如果设计稿为 750px,那么 1vw = 7.5px,100vw = 750px。其实设计稿按照设么都没多大关系,最终转化过来的都是相对单位,上面讲的 rem 也是对它的模拟。</p>
<blockquote>跟之前一样的痛点,我们仍然需要花费大量不必要的计算时间去把标注图中的px转换为vw,有没有类似于postcss-px2rem的工具呢?很荣幸能再次站在巨人的肩膀上,已经有大神写了了类似的PostCss插件 <a href="https://link.segmentfault.com/?enc=N7gACbStnxtOFRYQ%2FRcXEA%3D%3D.DD1qMhTEOxaNhgUPVWECFYYLnIZjUyqYcqqE8tFccM1tXGTj64VWQjHSD%2FbkABgKG7PbcVKSlg8eJ0l7rkGDFQ%3D%3D" rel="nofollow">postcss-px-to-viewport</a>
</blockquote>
<p>参看资料<br><a href="https://link.segmentfault.com/?enc=h3kGHJMD%2FL6fg%2BjcOHf%2FJA%3D%3D.Oet%2FNzEK90%2BxdF8GGJHIeW8n9yF%2FB8QNu1UERkkxWezwwH3b62rZw2EnM%2FxvA1aj9l5bnTr%2BpPTp9CzOk%2BlXmAgU7b3ZXGTSxkEZeEC13d4%3D" rel="nofollow">https://www.w3cplus.com/mobil...</a></p>
不一样的redux源码解读
https://segmentfault.com/a/1190000018452378
2019-03-10T21:32:58+08:00
2019-03-10T21:32:58+08:00
Alan
https://segmentfault.com/u/xc_xiang
1
<blockquote>1、本文不涉及redux的使用方法,因此可能更适合使用过 redux 的同学阅读<br>2、当前redux版本为4.0.1<br>3、<a href="https://link.segmentfault.com/?enc=qES8ZLsvGPpLlQwIR51P6g%3D%3D.Ndtb9jymxdjRwIZN3jtFU%2B6XCeWpvrircbgklF4QahmwrtLyPcy8rSQTXh8p1bkH" rel="nofollow">更多系列文章请看</a>
</blockquote>
<p>Redux作为大型React应用状态管理最常用的工具。虽然在平时的工作中很多次的用到了它,但是一直没有对其原理进行研究。最近看了一下源码,下面是我自己的一些简单认识,如有疑问欢迎交流。</p>
<h2>1、createStore</h2>
<p>结合使用场景我们首先来看一下<code>createStore</code>方法。</p>
<pre><code class="js">// 这是我们平常使用时创建store
const store = createStore(reducers, state, enhance);</code></pre>
<p>以下源码为去除异常校验后的源码,</p>
<pre><code class="js">
export default function createStore(reducer, preloadedState, enhancer) {
// 如果有传入合法的enhance,则通过enhancer再调用一次createStore
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState) // 这里涉及到中间件,后面介绍applyMiddleware时在具体介绍
}
let currentReducer = reducer //把 reducer 赋值给 currentReducer
let currentState = preloadedState //把 preloadedState 赋值给 currentState
let currentListeners = [] //初始化监听函数列表
let nextListeners = currentListeners //监听列表的一个引用
let isDispatching = false //是否正在dispatch
function ensureCanMutateNextListeners() {}
function getState() {}
function subscribe(listener) {}
function dispatch(action) {}
function replaceReducer(nextReducer) {}
// 在 creatorStore 内部没有看到此方法的调用,就不讲了
function observable() {}
//初始化 store 里的 state tree
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}</code></pre>
<p>我们可以看到creatorStore方法除了返回我们常用的方法外,还做了一次初始化过程<code>dispatch({ type: ActionTypes.INIT })</code>;那么dispatch干了什么事情呢?</p>
<pre><code class="js">/**
* dispath action。这是触发 state 变化的惟一途径。
* @param {Object} 一个普通(plain)的对象,对象当中必须有 type 属性
* @returns {Object} 返回 dispatch 的 action
*/
function dispatch(action) {
// 判断 dispahch 正在运行,Reducer在处理的时候又要执行 dispatch
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
//标记 dispatch 正在运行
isDispatching = true
//执行当前 Reducer 函数返回新的 state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
//遍历所有的监听函数
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener() // 执行每一个监听函数
}
return action
}</code></pre>
<p>这里<code>dispatch</code>主要做了二件事情</p>
<ul>
<li>通过<code>reducer</code>更新<code>state</code>
</li>
<li>执行所有的监听函数,通知状态的变更</li>
</ul>
<p>那么reducer是怎么改变state的呢?这就涉及到下面的<code>combineReducers</code>了</p>
<h2>2、combineReducers</h2>
<p>回到上面创建store的参数reducers,</p>
<pre><code class="js">// 两个reducer
const todos = (state = INIT.todos, action) => {
// ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
// ...
};
const reducers = combineReducers({
todos,
filterStatus
});
// 这是我们平常使用时创建store
const store = createStore(reducers, state, enhance);</code></pre>
<p>下面我们来看<code>combineReducers</code>做了什么</p>
<pre><code class="js">export default function combineReducers(reducers) {
// 第一次筛选,参数reducers为Object
// 筛选掉reducers中不是function的键值对
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 二次筛选,判断reducer中传入的值是否合法(!== undefined)
// 获取筛选完之后的所有key
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}
// 遍历所有的key和reducer,分别将reducer对应的key所代表的state,代入到reducer中进行函数调用
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
// 这里就是reducer function的名称和要和state同名的原因,传说中的黑魔法
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
// 将reducer返回的值填入nextState
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 发生改变了返回新的nextState,否则返回原先的state
return hasChanged ? nextState : state
}
}</code></pre>
<ul>
<li>这里 <code>reducer(previousStateForKey, action)</code>执行的就是我们上面定义的<code>todos</code>和<code>filterStatus</code>方法。通过这二个<code>reducer</code>改变<code>state</code>值。</li>
<li>以前我一直很奇怪我们的<code>reducer</code>里的<code>state</code>是怎么做到取当前<code>reducer</code>对应的数据。看到<code>const previousStateForKey = state[key]</code>这里我就明白了。</li>
<li>这里还有一个疑问点就是<code>combineReducers</code>的嵌套,最开始也我不明白,看了源码才知道<code>combineReducers()=> combination(state = {}, action)</code>,这里<code>combineReducers</code>返回的<code>combination</code>也是接受<code>(state = {}, action)</code>也就是一个<code>reducer</code>所以可以正常嵌套。</li>
</ul>
<p>看到这初始化流程已经走完了。这个过程我们认识了<code>dispatch</code>和<code>combineReducers</code>;接下来我们来看一下我们自己要怎么更新数据。</p>
<p>用户更新数据时,是通过<code>createStore</code>后暴露出来的<code>dispatch</code>方法来触发的。<code>dispatch </code>方法,是 <code>store </code>对象提供的更改 <code>currentState</code> 这个闭包变量的唯一建议途径(<strong>注意这里是唯一建议途径,不是唯一途径,因为通过getState获取到的是state的引用,所以是可以直接修改的。但是这样就不能更新视图了</strong>)。<br>正常情况下我们只需要像下面这样</p>
<pre><code class="js">//action creator
var addTodo = function(text){
return {
type: 'add_todo',
text: text
};
};
function TodoReducer(state = [], action){
switch (action.type) {
case 'add_todo':
return state.concat(action.text);
default:
return state;
}
};
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '读书'});// 或者下面这样
// store.dispatch(addTodo('读书'));</code></pre>
<p>也就是说<code>dispatch</code>接受一个包含<code>type</code>的对象。框架为我们提供了一个创建<code>Action</code>的方法<code>bindActionCreators</code></p>
<h2>3、bindActionCreators</h2>
<p>下面来看下源码</p>
<pre><code class="js">// 核心代码,并通过apply将this绑定起来
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
// 如果actionCreators是一个函数,则说明只有一个actionCreator,就直接调用bindActionCreator
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 遍历对象,然后对每个遍历项的 actionCreator 生成函数,将函数按照原来的 key 值放到一个对象中,最后返回这个对象
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}</code></pre>
<p><code>bindActionCreators</code>的作用就是使用<code>dispatch</code>把<code> action creator</code>包裹起来,这样我们就可以直接调用他们了。这个在平常开发中不常用。</p>
<h2>4、applyMiddleware</h2>
<p>最后我们回头来看一下之前调到的中间件,</p>
<pre><code class="js">import thunkMiddleware from 'redux-thunk';
// 两个reducer
const todos = (state = INIT.todos, action) => {
// ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
// ...
};
const reducers = combineReducers({
todos,
filterStatus
});
// 这是我们平常使用时创建store
const store = createStore(reducers, state, applyMiddleware(thunkMiddleware));</code></pre>
<p>为了下文好理解这个放一下<code>redux-thunk</code>的源码</p>
<pre><code class="js">function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;</code></pre>
<p>可以看出<code>thunk</code>返回了一个接受<code>({ dispatch, getState })</code>为参数的函数<br>下面我们来看一下<code>applyMiddleware</code>源码分析</p>
<pre><code class="js">export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 每个 middleware 都以 middlewareAPI 作为参数进行注入,返回一个新的链。此时的返回值相当于调用 thunkMiddleware 返回的函数: (next) => (action) => {} ,接收一个next作为其参数
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 并将链代入进 compose 组成一个函数的调用链
// compose(...chain) 返回形如(...args) => f(g(h(...args))),f/g/h都是chain中的函数对象。
// 在目前只有 thunkMiddleware 作为 middlewares 参数的情况下,将返回 (next) => (action) => {}
// 之后以 store.dispatch 作为参数进行注入注意这里这里的store.dispatch是没有被修改的dispatch他被传给了next;
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
// 定义一个代码组合的方法
// 传入一些function作为参数,返回其链式调用的形态。例如,
// compose(f, g, h) 最终返回 (...args) => f(g(h(...args)))
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
} else {
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
}</code></pre>
<p>我第一眼看去一脸闷逼,咋这么复杂。但是静下心来看也就是一个三级柯里化的函数,我们从头来分析一下这个过程</p>
<pre><code class="js">// createStore.js
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}</code></pre>
<p>也就是说,会变成这样</p>
<pre><code class="js">applyMiddleware(thunkMiddleware)(createStore)(reducer, preloadedState)</code></pre>
<ul><li><code>applyMiddleware(thunkMiddleware)</code></li></ul>
<p><code>applyMiddleware</code>接收<code>thunkMiddleware</code>作为参数,返回形如<code>(createStore) => (...args) => {}</code>的函数。</p>
<ul><li><code>applyMiddleware(thunkMiddleware)(createStore)</code></li></ul>
<p>以 <code>createStore</code> 作为参数,调用上一步返回的函数<code>(...args) => {}</code></p>
<ul><li><code>applyMiddleware(thunkMiddleware)(createStore)(reducer, preloadedState)</code></li></ul>
<p>以<code>(reducer, preloadedState)</code>为参数进行调用。 在这个函数内部,<code>thunkMiddleware</code>被调用,其作用是监测<code>type</code>是<code>function</code>的<code>action</code></p>
<p>因此,如果dispatch的action返回的是一个function,则证明是中间件,则将(dispatch, getState)作为参数代入其中,进行action 内部下一步的操作。否则的话,认为只是一个普通的action,将通过next(也就是dispatch)进一步分发</p>
<hr>
<p>也就是说,<code>applyMiddleware(thunkMiddleware)</code>作为<code>enhance</code>,最终起了这样的作用:</p>
<p>对<code>dispatch</code>调用的<code>action</code>进行检查,如果<code>action</code>在第一次调用之后返回的是<code>function</code>,则将<code>(dispatch, getState)</code>作为参数注入到<code>action</code>返回的方法中,否则就正常对action进行分发,这样一来我们的中间件就完成喽~</p>
<blockquote>因此,当action内部需要获取state,或者需要进行异步操作,在操作完成之后进行事件调用分发的话,我们就可以让action 返回一个以(dispatch, getState)为参数的function而不是通常的Object,enhance就会对其进行检测以便正确的处理</blockquote>
<p>到此<code>redux</code>源码的主要部分讲完了,如有感兴趣的同学可以去看一下我没讲到的一些东西转送门<a href="https://link.segmentfault.com/?enc=BM1%2BQSrNoCrD3JMeG0kpbQ%3D%3D.hBzLI7WGmLV%2F%2BY%2B9KPRiLpr%2FpEXIhGFFyA9%2BAaM9IfZtp%2Bbt9F6SMB0Wxr%2BsmfeCL3QjKKDjBJ6LrtPtf57GIw%3D%3D" rel="nofollow">redux</a>;</p>