Fork me on GitHub
image frame

Walter

面朝大海 春暖花开

DllPlugin和DllReferencePlugin

webpack4.0(六):DllPlugin和DllReferencePlugin

前言

在项目开发的时候,我们可能会发现有时候我们在打包的时候,构建速度很慢,尤其是一些我们使用的第三方库,每次都会被打包进去,大大延长了构建时间。那么我们能不能将这些稳定的库单独打包出来,这样既可以加快构建速度,还可以减少bundle的体积?

DllPlugin和DllReferencePlugin

答案是可以的。DLLPluginDLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。
这个插件是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为 manifest.json 的文件,这个文件是用来让 DllReferencePlugin 映射到相关的依赖上去的。

DllPlugin

该插件接受三个参数:

  • context (optional): manifest 文件中请求的上下文(context)(默认值为 webpack 的上下文(context))
  • name: 暴露出的 DLL 的函数名 ( [hash] & [name] )
  • path: manifest json 文件的绝对路径 (输出文件)

结果会在给定的 path 路径下创建一个名为 manifest.json 的文件。 这个文件包含了从 requireimport 的request到模块 id 的映射。 DLLReferencePlugin 也会用到这个文件。
webpack.dll.js:

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode:"production",
    entry:{
        react:['react','react-dom','react-router-dom'],
        lodash:['lodash-es']
    },
    output:{
        filename:'[name].dll.js',
        path : path.resolve(__dirname,'./dll'),
        library:'_dll_[name]'
    },
    plugins:[
        new webpack.DllPlugin({
            name: "_dll_[name]",
            path: path.join(__dirname, "./dll/[name].manifest.json"),
        })
    ]
}

DllReferencePlugin

这个插件是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(们)(dll-only-bundle(s)) 引用到需要的预编译的依赖。该插件接受以下参数:

  • context: (绝对路径) manifest (或者是内容属性)中请求的上下文
  • manifest: 包含 contentname 的对象,或者在编译时(compilation)的一个用于加载的 JSON manifest 绝对路径
  • content (optional): 请求到模块 id 的映射 (默认值为 manifest.content)
  • name (optional): dll 暴露的地方的名称 (默认值为 manifest.name) (可参考 externals)
  • scope (optional): dll 中内容的前缀
  • sourceType (optional): dll 是如何暴露的 (libraryTarget)

这里,我们引入一个新的插件:add-asset-html-webpack-plugin。它可以将一些资源文件绑定到html上。
这里用它就是将上面打包好的第三方库绑定到html上。

webpack.config.js:

const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

const files = fs.readdirSync(path.resolve(__dirname,'./dll'));
console.log(files);

let pluginsDll = [
    new HtmlWebpackPlugin({
        template: './src/index.html'
    })
];

files.forEach(file => {
    if(/.*\.dll.js$/.test(file)){

        pluginsDll.push(
            new AddAssetHtmlWebpackPlugin({
                filepath:path.resolve(__dirname,'./dll',file)
            })
        )
    }else if(/.*\.manifest.json$/.test(file)){
        new webpack.DllReferencePlugin({
            manifest: require(path.resolve(__dirname,'./dll',file)),
        })
    }
});

module.exports = {
    entry: './src/app.js',
    mode: "production",
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    resolve:{
        extensions:['.js','.jsx'],
        mainFiles:['main','index']
    },
    devServer:{
        contentBase:'./dist',
        open:true,
        hotOnly:true
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: 'babel-loader'
            }
        ]
    },
    plugins: pluginsDll
}

上面的代码中,我们使用fs模块获取dll文件夹下所有的文件,通过循环遍历,将资源全部挂载到html上。
这样,当打包的时候,如果本地的dll文件下有了第三方库的文件,打包的时候就不会再次向node_module上去取。大大加快了构建速度。

babel

webpack4.0(四):babel

什么是babel

ES6是ECMAScript在2015年发布的下一代JavaScript语法,它引入了一些新的语法和API,但是这些新的语法和API只有在最新的浏览器中才会得到支持,就算是现在仍然有一部分浏览器不支持ES6。所以我们需要一个转换工具,将ES6+的语法转换为ES5的语法。而babel就是这样一个转换工具。

本文的babel讲解基于babel7。

babel

babel在执行编译的时候,会从项目根目录下的.babelrc中或者babel-loader的options中读取其配置。.babelrc是一个json格式的文件,其中主要是对预设(presets)和 插件(plugins)的配置。

{
    "presets": [ 
        [
            "@babel/preset-env",
            {
                "targets": {
                    "edge": "17",
                    "firefox": "60",
                    "chrome": "67",
                    "safari": "11.1",
                },
                "useBuiltIns":"usage"//只转换已使用的模块
            }
        ]
    ],
    "plugins": []
}

babel-loader

关于babel-loader的具体配置,请查看官方文档

webpack 4.x | babel-loader 8.x | babel 7.x

安装:npm install -D babel-loader @babel/core @babel/preset-env webpack
配置:
在webpack.config.js中:

module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,//对于第三方的插件 无需做多余处理
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }

关于@babel/preset-env的配置:请移步
其实babel-loader只是babel和webpack沟通的桥梁,真正可以将ES6+语法转换为ES5语法的还是``@babel/preset-env
按照上述配置,已经可以将ES6+的语法转换为ES5了。但是@babel/preset-env只是将语法翻译成ES5 ,在低版本的浏览器中,不支持的API,如Promise、map等,仍然没有被补充进去。所以我们需要使用一个工具在低版本的浏览器中补充这些新的API。
@babel/polyfill@babel/runtime正是用来做这些工作的。

@babel/polyfill

安装:npm install --save @babel/polyfill
使用:在项目的入口文件中第一行引入即可:import "@babel/polyfill";。由于@babel/polyfill很大,所以在配合webpack使用的时候,最好在babelrc文件中添加一项配置:useBuiltIns: 'usage'。这样在打包编译的时候,就不会将为使用的ES6+语法也给打包进去了。

babel-plugin-transform-runtime

安装:具体配置请移步
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
配置:
.babelrc:

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false,
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}

@babel/polyfill 和 babel-plugin-transform-runtime的区别

  • 前者的原理是当运行环境中并没有实现的一些方法,babel-polyfill会做兼容。后者是将ES6语法转换为ES5的语法,不管浏览器支不支持ES6的语法,都会编译,所以会有很多的冗余的代码。
  • 前者是在全局环境一种添加方法,会造成全局变量的污染。后者它不会污染全局对象和内置对象的原型,比如说我们需要Promise,我们只需要import Promise from ‘babel-runtime/core-js/promise’即可,这样不仅避免污染全局对象,而且可以减少不必要的代码。

codeSplitting

webpack4.0(三):codeSplitting

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

当我们在做项目的时候,都会引入一些第三方的库来帮助我们开发,例如:lodash、underscore等等,但是这些库在我们打包的时候,会被默认打包到bundle.js中,极大的增加了js的体积,如下图:
image.png
直接把我们的js体积提升到了2.4M,这时我们需要将其从main.js中分割出来,减小main.js的体积。

常见的有三种方式进行代码分割:

  • 在entry手动的配置代码分割。
  • 使用SplitChunksPlugin进行公共代码提取和分割。
  • 动态导入。

entry手动分割

entry: {
    index:'./index.js',
    math:'./math.js'
},

执行打包之后,会被打包成不同的文件。
缺点:

  1. 如果不同的模块中引入有重复的模块,那么重复的模块就会被多次打包。
  2. 不够灵活,不能将核心逻辑进行动态的拆分。

SplitChunksPlugin

SplitChunksPlugin插件可以将相同的模块提取到一起,并且还可以将我们使用的第三方插件单独提取出来,优化打包之后的bundle.js的体积。
具体使用:具体配置请查看官方文档

optimization: {
  splitChunks: {
    chunks: "all",//进行代码分割的时候,all:针对所有的导入  async:只针对异步导入 initial:针对同步代码导入。
    minSize: 30000,//设置最小阀值,只有大于该阀值,才会进行代码分割。
    minChunks: 1,//在分割模块之前共享一个模块的最小块数(设置代码最少被引用次数)
    maxAsyncRequests: 5,//按需加载时的最大并行请求数 超过就不会在做代码分割打包
    maxInitialRequests: 3,//一个入口点的最大并行请求数  超过就不会做代码分割
    automaticNameDelimiter: '~',//打包生成之后,默认情况下,webpack将使用块的来源和名称来生成名称,比如vendor ~main.js。
    name: true,//使得cacheGroups中打包生成的文件名称
    cacheGroups: {//缓存组 打包分组
      vendors: {//配置同步导入 
        test: /[\\/]node_modules[\\/]/,//只有node_module中的才会进入
        priority: -10,//值越大 优先级越高
        filename: 'vendors.js'
      },
      default: {
        // minChunks: 2,
        priority: 0,
        reuseExistingChunk: true,// 如果模块已经被打包了,在此遇到的时候 直接忽略,直接使用以打包好的模块。
        filename:'default.js'
      }
          }
    }
}

动态导入

动态导入有两种方式:

  • 使用ECMAScript提案的import()语法
  • 使用webpack特定的语法:require.ensure()

第一种方式:使用webpackChunkName

function getComponent(){
    return import(/* webpackChunkName:"lodash" */ "lodash").then(({ default:_ }) => {
        let div = document.createElement('div');
        div.innerHTML = _.join(['hello','world','lodash']);
        return div;
    })
};

getComponent().then((ele) => {
    document.body.appendChild(ele)
})

打包之后,会在dist目录下生成一个vendors~lodash.js文件,表示它是vendors下面的一个文件。之所以前面会带一个vendors,这是因为cacheGroup有一个默认配置,如果想要去除,将vendors和default配置为false即可。
由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。但是,需要使用像 Babel 这样的预处理器和Syntax Dynamic Import Babel Plugin

webpackDevServer

webpack4.0(二):webpackDevServer

在开发中我们还需要一个本地文件的服务器,并且当我们保存代码的时候会自动进行打包,并且还支持 Source Map,以方便代码调试等功能,因此我们现在需要使用到 devServer了。

webpack-dev-server

首先我们需要安装:webpack-dev-server:

全局安装:
npm install -g webpack-dev-server
局部安装:
npm install --save-dev webpack-dev-server

在webpack4之后,同时需要安装:webpack-cli

npm install webpack-cli -D

最后我们运行webpack-dev-server,就可以启动一个本地服务器,默认端口号是8080,访问http://localhost:8080/即可。

注意:使用webpack-dev-server打包的文件保存在电脑内存当中,可以提高访问速度。

当然,这个默认的配置是可以修改的,在webpack.config.js中:

devServer:{
    contentBase: './dist',//指定服务器默认打开的文件夹
  open: true,//是否自动打开浏览器窗口
  port: "8081"//配置端口号
}

watch

实时预览。使用webpack --watch,监听文件系统的变化,实现自动打包。当项目的入口文件或者依赖文件发生变动时,它会重新构建,构建完成之后会刷新页面。但是如果修改的不是入口文件或者依赖文件,它是不会重新构建的。

webpack-dev-middleware

新建一个服务端,并且将webpack处理过的文件传送给服务器。
原理:使用webpack和其对应的配置文件,生成一个编译器,使用该中间件,在服务器上监听文件变化,重新打包。
使用express建立本地服务器:

//server.js
const express = require('express');
const webpack = require('webpack');
const middleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const complier = webpack(config);//编译器
const app = express();

app.use(middleware(complier, {
    publicPath: config.output.publicPath
}))

app.listen(3000)

然后访问3000端口即可。

HMR

除了以上方法可以实现在入口文件或者依赖文件发生变动时进行重新打包加载页面之外,使用模块热替换技术也可以实现在不刷新页面的情况下,使用已更新的模块替换旧模块。
模块热替换技术默认是不开启的,我们可以通过以下方法开启:

  1. 在启动devServer的时候带上--inline 或者 --inline hot即可。
  2. 在devServer中配置:
devServer: {
  contentBase: './dist',
  open: true,
  port: 8080,
  hot: true, //配置HMR 阻止浏览器自动刷新
  hotOnly: true, //即便HMR不生效 也不让浏览器自动刷新
},
 plugins: [
    new webpack.HotModuleReplacementPlugin()
]

proxy

dev-server使用了非常强大的包:http-proxy-middleware,具体用法请查看使用文档
举个🌰:如果在http://localhost:3000上有后端开发服务器,我们可以这样设置代理:

devServer:{
    proxy:{
      "/api":"http://localhost:3000/"
  }
}

将请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users
如果不想始终传递 /api ,则需要重写路径:

devServer:{
    proxy: {
    "/api": {
      target: "http://localhost:3000",
      pathRewrite: {"^/api" : ""}
    }
  }
}

默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,修改配置如下:

devServer:{
    proxy: {
    "/api": {
      target: "http://localhost:3000",
      pathRewrite: {"^/api" : ""},
      secure: false
    }
  }
}

有时你不想代理所有的请求。可以基于一个函数的返回值绕过代理。
在函数中你可以访问请求体、响应体和代理选项。必须返回 false 或路径,来跳过代理请求。
例如:对于浏览器请求,你想要提供一个 HTML 页面,但是对于 API 请求则保持代理。你可以这样做:

proxy: {
  "/api": {
    target: "http://localhost:3000",
    bypass: function(req, res, proxyOptions) {
      if (req.headers.accept.indexOf("html") !== -1) {
        console.log("Skipping proxy for browser request.");
        return "/index.html";
      }
    }
  }
}

如果想要代理多个目标路径大同一目标呢服务器上,则可以使用数组或者多个对象的形式:

proxy: [{
  context: ["/auth", "/api"],
  target: "http://localhost:3000",
}]

//或者

proxy: {
  "/api":{
      "target":"http://localhost:3000/"
  },
  "/auth":{
      "target":"http://localhost:4000/"
  }
}

浏览器进程与线程

浏览器进程和线程

浏览器

浏览器是多进程的。每一个Tab页就是一个独立的进程。

  • 浏览器进程(主进程):个人理解为“管家”的角色,它控制着浏览器的各个任务,页面展示、网络请求、历史回退、前进等等
  • 第三方插件进程:管理第三方插件
  • GPU线程:用于3D绘制
  • 渲染进程:浏览器内核,多线程。主要负责页面渲染、脚本执行、事件循环等。对前端来说,最重要的进程。

浏览器多进程优势:

  • 防止单个页面或者插件崩溃影响整个浏览器的运行。
  • 可以充分利用浏览器多核的优势。
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性。

浏览器内核(渲染进程)

渲染进程是多线程的。它包含一下几大线程:

  • GUI线程
    • 它负责页面的渲染、布局、解析HTML、CSS、渲染DOM树。
    • 当页面发生重绘或者回流的时候,就会触发该线程
    • 它和JS引擎线程互斥
  • JS引擎线程
    • 负责程序的解析和执行
  • 事件触发线程
    • 它控制事件循环,管理着一个事件任务队列TaskQueue
    • 当异步任务满足条件时,会将该异步任务的回调函数放入到JS引擎线程所在的执行栈中执行
  • 定时触发器线程
    • setTimeOut 和 setInterval 所在的线程
    • 定时器任务的计时不是由JS引擎计时的,而是由该线程控制
    • 当定时任务计时完成之后,会通知事件触发线程
    • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  • 异步http请求线程
    • 一个独立的ajax请求线程
    • 当请求完成之后,会通知事件触发线程

为什么GUI线程和JS引擎线程互斥

因为JS是可以操作DOM的,如果使用JS操作DOM的同时,GUI线程也在渲染DOM,那么渲染完成之后的元素可能就不是之前的元素了

为什么JS引擎是单线程

  • 创建JavaScript语言的时候,多进程多线程的架构并不流行,硬件支持度不高
  • 多进程多线程操作需要加锁,操作成本较高,较为复杂
  • 如果多个线程同时操作一个DOM,那么结果会是不可预料的
  • 线程之间资源共享

Event Loop

  • JS的任务分为同步任何和异步任务的
  • 同步任务都在JS引擎的执行栈上执行
  • 事件触发线程管理一个任务队列,TaskQueue,当异步任务满足条件时,事件触发线程会将其放到任务队列当中,当主线程(JS引擎线程)执行完执行栈中的任务之后,会读取任务队列中的任务,如果有,就将可执行的异步任务的回调函数推入到执行栈中,开始执行。如果没有,则再次向事件触发线程发起询问,直到有为止。

代码解释:

let timerCallback = function() {
  console.log('timerCallback');
};
let httpCallback = function() {
  console.log('httpCallback');
}

// 同步任务
console.log('同步任务1');
// 同步任务
// 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理
// 1s 后事件触发线程将 timerCallback 加入到事件队列中
setTimeout(timerCallback,1000);
// 同步任务
// 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理
// 请求成功后事件触发线程将 httpCallback 加入到事件队列中
$.get('www.xxxx.com',httpCallback);
// 同步任务
console.log('同步任务2');
//...
// 所有同步任务执行完后
// 询问事件触发线程在事件事件队列中是否有需要执行的回调函数
// 如果没有,一直询问,直到有为止
// 如果有,将回调事件加入执行栈中,开始执行回调代码

总结:

  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

宏任务、微任务

宏任务:可以理解为浏览器级别的任务。主代码块,setTimeout,setInterval等,都属于宏任务

微任务:JS引擎级别的任务。Promise,process.nextTick等,属于微任务

执行顺序:

  • 当前代码块对应的宏任务
  • 当前宏任务结束之后,下一个宏任务开始之前,执行当前对列的微任务
  • GUI引擎渲染
  • 下一个宏任务

Koa2洋葱模型

浅解析洋葱模型

koa2最大的特点就是独特的中间件控制流程。koa2基于NodeV7.6.0,可以直接使用async/await来替代generator,使得代码彻底摆脱“回调地狱”,可读性更佳。

洋葱模型

以下是一张中间件的“神图”,它可以清晰的表示出:从代码请求到响应的过程中,中间件的工作流程。
image.png

小demo

先看个小🌰吧:

const Koa = require('koa');

const app = new Koa();

app.use(async(ctx, next) => {
    console.log(1)
    await next();
    console.log(2)
});

app.use(async(ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
});

app.use(async(ctx, next) => {
    console.log(5)
});

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
});
//1 3 5 4 2

启动demo,访问服务器地址,控制台就会输出答案: 1、3、5、4、2 。
从这个🌰中,我们可以看出,当程序执行遇到await next() 的时候,会暂停执行当前代码块,进入下一个中间件,等下一个中间件执行完之后再回过头来继续处理。

小结

当在一个中间件中调用await next()时,当前中间件就会把程序的控制权交给下一个中间件。当下游没有中间件等待执行的时候,程序就会回溯上游,执行上游中间件等待执行的行为。

源码解析

koa2的源码只有四个文件,详情请查看Koa2源码

  • application.js:入口文件。
  • context.js:应用上下文文件。
  • request.js:请求处理文件。
  • response.js:响应处理文件。

先从入口文件看起:

application.js

/**
     * Shorthand for:
     *
     *    http.createServer(app.callback()).listen(...)
     *
     * @param {Mixed} ...
     * @return {Server}
     * @api public
     */

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

在入口文件中,我们可以看到,其实我们经常使用的app.listen(port),只是一个“语法糖”,它只是使用来 this.callback() 来生成nodehttpServer的回调函数。我们来看this.callback()

/**
     * Return a request handler callback
     * for node's native http server.
     *
     * @return {Function}
     * @api public
     */

callback() {
  const fn = compose(this.middleware);

  if (!this.listenerCount('error')) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

在这段代码中,首先使用compose将中间件组合起来,compose就是一个柯里化函数,它接受中间件数组,返回一个函数,在返回函数中,接受应用上下文ctx和next,返回一个promise对象。
compose源码

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

compose函数中,最重要的就是使用diapatch()函数,遍历中间件数组,然后将ctx和dispatch(i + 1)传递给中间件的方法中。它做到了以下两点:

  • 将ctx应用文对象一直传递下去。
  • 将下一个中间件函数作为未来的next的返回值。

this.callback()中也使用了createContext()handleRequest()方法,我们来看下它们做来什么。

/**
     * Handle request in callback.
     *
     * @api private
     */

    handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }

    /**
     * Initialize a new context.
     *
     * @api private
     */

    createContext(req, res) {
        const context = Object.create(this.context);
        const request = context.request = Object.create(this.request);
        const response = context.response = Object.create(this.response);
        context.app = request.app = response.app = this;
        context.req = request.req = response.req = req;
        context.res = request.res = response.res = res;
        request.ctx = response.ctx = context;
        request.response = response;
        response.request = request;
        context.originalUrl = request.originalUrl = req.url;
        context.state = {};
        return context;
    }

createContext()中,返回一个应用上下文对象context。在handleRequest()中,将应用上下文ctx和中间件进行绑定。
在Koa2中,我们使用中间件的时候需要使用app.use()来注册中间件,那么app.use()到底做来什么呢?

 /**
     * Use the given middleware `fn`.
     *
     * Old-style middleware will be converted.
     *
     * @param {Function} fn
     * @return {Application} self
     * @api public
     */

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

去除注释和验证,它只做了两件事:

  • 将中间件函数push到中间件数组(栈)。
  • 返回koa实例对象。
    use(fn) {
    this.middleware.push(fn);
    return this;
    }

总结

所谓“洋葱模型”,就是当执行中间件函数的时候,如果碰到await next(),就会执行下一个中间件(上面说过,中间件注册的时候,会把下一个中间件当作未来的next()函数),这里next()就是下一个中间件函数。

Koa2基础

Koa2学习笔记

Koa2简介

Koa是一款新的web框架,它是由Express原班人马打造,相比与Express,Koa更加mini、简洁、健壮。它没有绑定任何中间件,如果我们需要,我们完全可以手撸任何中间件。而且,Koa2通过使用async/await,使得代码的可读性更佳。

安装

Koa2依赖于nodeV7.6.0或者ES2015及更高的版本和async的支持。

#初始化package.json
npm init
#安装npm7
npm install 7
#安装koa2
npm install koa

async/await 小demo

async function test(time){
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('test async')
        },time)
    })
}

function getSomething(){
    return 'something'
}
//async await简单案例
async function fn(){
    const res1 = await test(2000);
    const res2 = await getSomething();
    console.log(res1,res2)
}
fn();

2s之后,依次打印test async something
await必须存在于async函数之内,且await后面的代码会被“阻塞”,等待await后面的promise执行。

  • 可以让异步逻辑用同步写法实现
  • await返回的需要是promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

    Hello world

const Koa = require('koa');
const app = new Koa();
//入门🌰
app.use(async(ctx) => {
    ctx.body = 'hello kaka'
    console.log('hello koa')
})

app.listen(3000)

启动demo,node app.js,打开http://localhost:3000/,即可看到:
image.png

中间件

kao中间件开发与使用

中间件的类型有一下四种:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 第三方中间件

    洋葱模型

    当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。
    当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。
    看🌰:
const Koa = require('koa');

const app = new Koa();

//洋葱模型

// #1
app.use(async(ctx, next) => {
    console.log(1)
    await next();
    console.log(2)
});
// #2
app.use(async(ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
});

app.use(async(ctx, next) => {
    console.log(5)
});

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
});
//1 3 5 4 2

image.png

开发

//middleware/logger.js
module.exports = async(ctx, next) => {
    ctx.state.commondata = '我是应用级中间件';
    console.log("应用级中间件");
    await next()
}

使用

//app.js
const Koa = require('koa');

const logger = require('./middleware/logger');

const app = new Koa();

//应用级中间件
app.use(logger)

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
})

路由

原生实现

const Koa = require('koa');
const app = new Koa();
const bodyparser = require('koa-bodyparser');
const fs = require('fs');

app.use(bodyparser());

/**
 * @param { string } 文件名
 * @return { promise } 返回文件
 * 
 */
function render(page) {
    return new Promise((resolve, reject) => {
        let pagePath = `./html/${page}`;
        console.log(pagePath)
        fs.readFile(pagePath, 'utf-8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        })
    })
}

/**
 * @param { string } 访问路由
 * @return { string }  html
*/
async function getHtml(url) {
    let page = '404.html';
    console.log(url)
    switch (url) {
        case '/':
            {
                page = 'index.html';
                break
            }
        case '/list':
            {
                page = 'list.html';
                break
            }
        default:
            {
                page = '404.html';
                break
            }
    }
    let html = await render(page);
    return html
}
app.use(async(ctx) => {
    //读取文件是异步
    let html = await getHtml(ctx.url);
    ctx.body = html;
})
app.listen(3000, () => {
    console.log("server is running at http://127.0.0.1:3000")
})

启动demo之后,访问http://localhost:3000/即可
image.png

koa-router

安装对应的koa-router中间件:
npm install koa-router
demo源码

const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router');

const app = new Koa();
const router = new Router({
    //设置路由前缀 
    prefix:'/kaka'
});

router.get('/',(ctx,next) => {
    ctx.body = 'hello';
    next()
})

app.use(router.routes()).use(router.allowedMethods())

app.use(async(ctx) => {

});

app.listen(3000,() => {
    console.log("server is running at http://127.0.0.1:3000")
})

请求数据处理

Get请求

在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。

  • 是从上下文中直接获取
  • 是从上下文的request对象中获取

看🌰: 源码

const Koa = require('koa');
const app = new Koa();
const bodyParse = require('koa-bodyparser');
//使用中间件koa-bodyparser
app.use(bodyParse());

app.use(async(ctx) => {
    const request = ctx.request;
    const url = ctx.url;
    const method = request.method;
    const query = request.query; //返回的是格式化好的参数对象。
    const querystring = request.querystring; //返回的是请求字符串。
    //直接从ctx中获取query
    const ctx_query = ctx.query;
    const ctx_querystring = ctx.querystring;
    if (method === 'GET' && url === '/') {
        ctx.body = `
        <h1>Koa2 request post demo</h1>
        <form method="POST"  action="/">
            <p>userName</p>
            <input name="userName" /> <br/>
            <p>age</p>
            <input name="age" /> <br/>
            <p>webSite</p>
            <input name='webSite' /><br/>
            <button type="submit">submit</button>
        </form>
        `
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        // const postDate = await handlePostData(ctx);
        const postDate = ctx.request.body;
        ctx.body = postDate;
    } else {
        //其它请求显示404页面
        ctx.body = '<h1>404!</h1>';
    }
    ctx.body = {
        url,
        methods,
        query,
        querystring,
        ctx_query,
        ctx_querystring
    }
    console.log(ctx)
});

app.listen(3000)

Post请求

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string,再将query string 解析成JSON格式。

//使用node中的方式处理post请求数据
function handlePostData(ctx) {
    return new Promise((resolve, reject) => {
        try {
            let querystring = '';
            ctx.req.on('data', (data) => {
                querystring += data;
            })
            ctx.req.on('end', () => {
                let bodyParse = parseStringData(querystring)
                resolve(bodyParse)
            })
        } catch (error) {
            reject(error)
        }
    })
}

function parseStringData(string) {
    let item = {};
    let stringList = string.split('&');
    for (const [key, value] of stringList.entries()) {
        console.log(key, value);
        item[value.split('=')[0]] = decodeURIComponent(value.split('=')[1]);
    }
    return item;
}

使用koa-parser中间件

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中
安装:npm install koa-parser
使用:

const bodyParse = require('koa-bodyparser');
//使用中间件koa-bodyparser
app.use(bodyParse());

静态资源处理

koa-static中间件

安装:npm install koa-static

const Koa = require('koa');
const static = require('koa-static');
const path = require('path');

const app = new Koa();

app.use(static(
    path.join(__dirname,'./static')
));

app.use( async ( ctx ) => {
    ctx.body = 'hello world'
  })

app.listen(3000, () => {
  console.log('static-use-middleware is starting at port 3000')
})

koa提供了直接操作cookie的方法:

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

使用:

const Koa =require('koa');

const app = new Koa();

app.use(async(ctx) => {
    /**
     * domain:写入cookie所在的域名
     * path:写入cookie所在的路径
     * maxAge:Cookie最大有效时长
     * expires:cookie失效时间
     * httpOnly:是否只用http请求中获得
     * overwirte:是否允许重写
    */
   if(ctx.url === '/'){
        ctx.cookies.set(
            "name","ykx",{
                domain:'127.0.0.1',
                maxAge:1000*60*60*24*30,
                expires:new Date('2020-12-30'),
                httpOnly:false,
                overwrite:true
            }
        );
        ctx.body = 'cookie is ready'
   }else{
       if(ctx.cookies.get('name')){
            const str = ctx.cookies.get('name');
            ctx.body = str;
       }else{
            ctx.body = 'cookies is none';
       }

   }

})

app.listen(3000);

session

koa原生只提供了操作cookie的方法,但是没有session操作。所以对sessino的操作需要自己实现或者使用第三方插件。
将session存储到数据库,需要使用第三方插件:

  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。

安装:

npm install koa-session-minimal koa-mysql-session --save

过程:

  • 生成session,并存储到数据库。
  • 使用插件操作数据库,并将sessionId和对应的数据存储到数据库。
  • 将数据库的sessionId写入到页面的cookie中。
  • 根据页面cookie携带的sessionId取出数据库中对应的数据。

看代码:

const Koa = require('koa');
const session = require("koa-session-minimal");
const MysqlSession = require("koa-mysql-session");

const app = new Koa();

// 配置存储session信息的mysql
let store = new MysqlSession({
    user: 'root',//数据库用户
    password: '',//密码
    database: 'test',//数据库名称
    host: '127.0.0.1',//数据库地址
});

// 存放sessionId的cookie配置
let cookie = {
    maxAge: '', // cookie有效时长
    expires: '', // cookie失效时间
    path: '', // 写cookie所在的路径
    domain: '', // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '', // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',

};

app.use(session({
    key: 'SESSION_ID',
    store,
    cookie
}));

app.use(async(ctx) => {
    // 设置session
    if (ctx.url === '/set') {
        ctx.session = {
            user_id: Math.random().toString(36).substr(2),
            count: 0
        }
        ctx.body = ctx.session
    } else if (ctx.url === '/') {
        // 读取session信息
        ctx.session.count = ctx.session.count + 1
        ctx.body = ctx.session
    }
})

app.listen(3000)
console.log('[demo] session is starting at port 3000')

模版引擎ejs

ejs

具体使用请查看官方文档

koa2使用ejs

安装:

#安装koa模版使用中间件
npm install koa-views --save
#安装ejs模版引擎
npm install ejs --save

使用:

const Koa = require('koa');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
//加载ejs模版 设置模版文件夹
app.use(views(path.join(__dirname,'./views'),{
    extension:'ejs'
}))

app.use(async(ctx) => {
    let title = 'hello ejs';
      //渲染模版
    await ctx.render('index',{
        title
    })
})

app.listen(3000,()=>{
    console.log('server is starting at 127.0.0.1:3000');
})

views下的模版:

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

文件上传

busboy

busboy用来解析Post请求,读取原生node的req中的文件流。具体使用请移步
安装:
npm install busboy --save
使用:源码
app.js

//文件上传
const Koa = require('koa');
const Views = require('koa-views');
const Router = require('koa-router');
const path = require('path');
const upload = require('./util/syncUpload.js');
const static = require('koa-static');



const app = new Koa();
const router = new Router();

app.use(Views(path.join(__dirname + '/views'), {
    extension: 'ejs'
}));
app.use(static('static'))

router.get('/', async ctx => {
    await ctx.render('upload')
});

router.post('/upload', async ctx => {
    let res = await upload(ctx, {
        path: path.join(__dirname, 'static')
    });
    ctx.body = '<img src="' + res.path + '" style="max-width: 100%">';
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000)

ejs上传模版:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>文件上传</title>
</head>

<body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <label for="file">文件</label>
        <input type="file" name="file" id="file">
        <input type="submit">
    </form>
</body>

</html>

node上传处理文件

const fs = require('fs');
const path = require('path');
const BusBoy = require('busboy');

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getFileSuffixName(fileName) {
    const nameList = fileName.split('.');
    return nameList[nameList.length - 1];
}


/**
 * 同步创建文件目录
 * @param { string } filePath 文件上传路径
 * @return { boolean }         返回结果
 */
function mkdirPath(dirname) {
    if (fs.existsSync(dirname)) {
        return true;
    }
    fs.mkdirSync(dirname)
    return true;
}

/**
 * 同步上传文件
 * @param { ctx } Object 上下文对象
 * @param { options } Object 配置参数
 * @return { promise }   返回上传结果的promise
 */
module.exports = (ctx, options) => {
    return new Promise((resolve, reject) => {
        let req = ctx.req;
        let res = ctx.res;
        let busboy = new BusBoy({ headers: req.headers });
        let result = {
            success: false,
            path: null
        }

        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
            console.log('File [' + fieldname + ']: filename: ' + filename + 'encoding: ' + encoding + 'mimetype: ' + mimetype);
            let fileName = Math.random().toString(32).substr(2) + '.' + getFileSuffixName(filename);
            // let filePath = mkdirPath(options.path) && (options.path + '/' + filename);
            let filePath = filename;
            file.on('data', data => {
                console.log('File [' + filename + '] got ' + data.length + ' bytes')
            });
            file.on('end', () => {
                console.log('File [' + filename + '] finished ');
                result.success = true;
                result.path = filePath;
                resolve(result)
            });
            file.on('error', () => {
                reject(result);
            });
            // 解析表单中其他字段信息
            busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
                // console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
                // result.formData[fieldname] = inspect(val);
            });

            // 解析结束事件
            busboy.on('finish', function() {
                console.log('文件上传结束')
                resolve(result)
            });

            // 解析错误事件
            busboy.on('error', function(err) {
                console.log('文件上传出错')
                reject(result)
            });
            file.pipe(fs.createWriteStream(filePath));
        })
        req.pipe(busboy);
    })
}

图片优化

图片优化

为什么要做图片优化?

Google官方的最佳实践中关于图片优化有下面这样一段描述:

对于网页来说,在所下载的字节数中,图片往往会占很大比例。因此,优化图片通常可以卓有成效地减少字节数和改进性能:浏览器需要下载的字节数越少,对客户端带宽的争用就越少,浏览器下载内容并在屏幕上呈现内容的速度就越快。

我们可以通过一些第三方方法来看一下在一个网站中,图片所占比例的大小:使用webTest网站,可以清楚的看到一个网站首屏加载时,图片所占比例的大小。而且电商网站中,图片所占比例占比是最大的。

通过优化图片可以减少客户端下载的字节数,改进网页性能。

图片类型和使用场景

一般常用的图片格式有那么几种:JPEG/JPG、PNG、WebP、Base64、SVG。

JPEG和JPG格式有什么区别?

没有区别,全名、正式扩展名是JPEG。但因DOS、Windows 95等早期系统采用的8.3命名规则只支持最长3字符的扩展名,为了兼容采用了.jpg。也因历史习惯和兼容性考虑,.jpg目前更流行。

JPEG/JPG

特点:有损压缩,体积小,加载快,不支持透明。

适用场景:适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。

PNG

特点:无损压缩,体积大,质量高,支持透明。

适用场景:PNG 在处理线条和颜色对比度方面有优势,主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等

SVG

特点:文本文件,体积线,不失真,兼容性好。

适用场景:将 SVG 写入独立文件后引入 HTML

<img src="文件名.svg" alt="">

Base64

特点:文本文件、依赖编码、小图标解决方案。

适用场景:小图标,更新频率非常低,作为雪碧图的补充。

WebP

WebP 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩。

特点:全能,但是有兼容性问题。

适用场景:限制我们使用 WebP 的最大问题不是“这个图片是否适合用 WebP 呈现”,而是“浏览器是否允许 WebP”。

图片优化方案

减少图片体积

使用WebP的方式,都可以在不降低图片质量的条件下,减少图片体积的。

WebP 的优势是它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。

cloudconvert可以在线转换WebP图片。

在淘宝首页,就有一些图片就是WebP类型的。淘宝是根据浏览器是否支持WebP来动态的设置图片url的。

那么如何判断浏览器是否支持WebP呢?

Google官方文档是这样处理的(先加载一个WebP图片,如果能获取到图片的宽度和高度,就说明是支持WebP的,反之则不支持):

function check_webp_feature(feature, callback) {
    var kTestImages = {
        lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
        lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
        alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==",
        animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"
    };
    var img = new Image();
    img.onload = function () {
        var result = (img.width > 0) && (img.height > 0);
        callback(feature, result);
    };
    img.onerror = function () {
        callback(feature, false);
    };
    img.src = "data:image/webp;base64," + kTestImages[feature];
}

原理:对每一种格式的webp(有损,无损,alpha通道,动态)生成一个很小像素的图片,在浏览器中渲染,如果没有问题,就代表支持webp。

还有一种方法,就是让服务器来判断浏览器是否支持WebP格式:服务器通过HTTP请求头的Accept字段,来判断返回什么格式的图片。当 Accept 字段包含 image/webp 时,就返回 WebP 格式的图片,否则返回原图。

利用SVGO压缩svg文件的大小。

缓存

图片懒加载

懒加载的原理是:将图片的url赋值给img的data-url(可自定义)属性,监听滚动事件,当图片进入可视区域时,将data-url的值赋值给img的url属性,这时才真正发出图片的http请求。

// 简单的节流函数
//fun 要执行的函数
//delay 延迟
//time  在time时间内必须执行一次
function throttle(fun, delay, time) {
    var timeout,
        startTime = new Date();

    return function() {
        var context = this,
            args = arguments,
            curTime = new Date();

        clearTimeout(timeout);
        // 如果达到了规定的触发时间间隔,触发 handler
        if (curTime - startTime >= time) {
            fun.apply(context, args);
            startTime = curTime;
            // 没达到触发间隔,重新设定定时器
        } else {
            timeout = setTimeout(fun, delay);
        }
    };
};
// 实际想绑定在 scroll 事件上的 handler
function lazyload(event) {
    for (var i = n; i < imgNum; i++) {
        if (img.eq(i).offset().top < parseInt($(window).height()) + parseInt($(window).scrollTop())) {
            if (img.eq(i).attr("src") == "default.jpg") {
                var src = img.eq(i).attr("data-src");
                img.eq(i).attr("src", src);

                n = i + 1;
            }
        }
    }
}
// 采用了节流函数
window.addEventListener('scroll', throttle(lazyload, 500, 1000));

响应式图片

一种可以在不同的屏幕尺寸和分辨率的设备上都能良好工作以及其他特性的图片.

艺术方向

当你想为不同布局提供不同剪裁的图片——比如在桌面布局上显示完整的、横向图片,而在手机布局上显示一张剪裁过的、突出重点的纵向图片,可以用 元素来实现。

<picture>
  <source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg">
  <source media="(min-width: 800px)" srcset="elva-800w.jpg">
  <img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva">
</picture>

分辨率切换

当你想要为窄屏提供更小的图片时,因为小屏幕不需要像桌面端显示那么大的图片;以及你想为高/低分辨率屏幕提供不同分辨率的图片时,都可以通过 vector graphics (SVG images)、 srcset 以及 sizes 属性来实现。

分辨率切换:不同的尺寸

<img srcset="elva-fairy-320w.jpg 320w,
             elva-fairy-480w.jpg 480w,
             elva-fairy-800w.jpg 800w"
     sizes="(max-width: 320px) 280px,
            (max-width: 480px) 440px,
            800px"
     src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">

分辨率切换: 相同的尺寸, 不同的分辨率

<img srcset="elva-fairy-320w.jpg,
             elva-fairy-480w.jpg 1.5x,
             elva-fairy-640w.jpg 2x"
     src="elva-fairy-640w.jpg" alt="Elva dressed as a fairy">

一般情况下当我们给img设置固定的宽的时候,为了在不同的dpr设备上显示相同的大小,可以配合x来,例如1.5x代表dpr为1.5的设备

srcset/sizes/ 新特性

已经被新版本的现代浏览器和移动浏览器所支 持(包括Edge,而不是IE)。

  • srcset语法: “图片路径 物理像素, 图片路径 物理像素, …..”。
  • size: “媒体查询 css像素”。
  • src: 当浏览器不支持上面的属性的时候,默认加载这里。

加载的流程如下:

  • 查看设备宽度
  • 查看sizes中哪一个媒体查询条件为真。
  • 查看给予该媒体查询的槽大小。
  • 加载srcset列表中引用的最接近所选的槽大小的图像

一般 1px = 1w

其他方案

雪碧图

base64编码内联小图片

cdn加速

HTTP缓存

HTTP缓存

HTTP报文

HTTP报文:作为浏览器和服务器通讯时发送和响应的数据块。浏览器向服务器发送数据时会发送请求报文(Request),服务器响应浏览器的请求时,会发送响应报文(Response)。

请求报文:包含请求方法、统一资源定位符(URL)、协议版本号、MIME信息等。

响应报文:包含协议版本号、响应状态码等信息。

缓存规则解析

当用户第一次访问页面的时候,浏览器的缓存数据库是没有缓存的,需要向服务器直接请求数据,服务器将会把资源和缓存规则返回给浏览器,浏览器将把资源和缓存规则进行缓存。具体如图所示:

强缓存(from disk cache 200)

已存在缓存时,请求资源流程如下:

从中可以看出,强缓存如果缓存数据库中已存在数据且未失效,那么将不会和服务器进行交互,直接从资源数据库中取。

对于强缓存来说,响应头将会有两个字端来标识失效规则:Expires/Cache-Control

Expires:Http1.0。它的值为服务端返回的到期时间。即下一次请求时的时间小于该时间的时候,直接使用缓存数据。

Cache-Control:Http1.1 。Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

private:客户端可以缓存。

public: 客户端和代理服务器都可缓存。

max-age=xxx:缓存的内容将在 xxx 秒后失效。

no-cache:需要使用对比缓存来验证缓存数据。

no-store:所有内容都不会缓存,强制缓存,对比缓存都不会触发。

对比缓存(304)

已存在缓存数据时,请求资源的流程:

对比缓存,顾名思义,需要进行比较判断是否可以使用缓存。

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。

再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

在对比缓存生效时,状态码为304,并且报文大小和请求时间大大减少。

原因是,服务端在进行标识比较后,只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端。

If-Modified-Since/Last-Modified:Http1.0 。浏览器在访问服务器的时候,服务器返回的响应报头中会携带Last-Modified字端,告诉浏览器文件最后修改的时间。当浏览器在此访问服务器的时候,会携带If-Modified-Since,告诉服务器上次文件修改的时间,服务器收到该字段之后,和服务器文件资源的最后修改时间做对比,判断资源是否有改动。若改动过,则返回200,并将整个资源返回。若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到1s以内

If-None-Match/E-tag:Http1.1 。服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtag INode Mtime Size生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。

块级作用域的函数声明和函数声明

块级作用域的变量声明和函数声明

最近在一个讨论群看到一个面试题:

{
  a = 10;
  function a(){};
  console.log(a)//10
};
console.log(a);//10

what?不应该是:function a(){} 、function a(){} 吗?

最后一位牛人给出了他的理解,这里本人只是作出验证,加深理解。

接下来的环境默认都是chrome环境,支持ES6。
下文提出的默认变量指的是没有使用 var、let、const定义的变量。

块级作用域的默认变量

我们先看这样一个🌰:

console.log(a);//ReferenceError: a is not defined
{
    a = 10;
    console.log(a)//10
}
console.log(a);//10

可能有些人会像我一样感到诧异:为什么第一个a打印出来的不是undefined?a = 10默认不是 var a = 10吗?应该会提升到全局作用域的顶端的啊。

那好,让我们把window.a打印出来看看:

console.log(window.a);//undefined
{
  console.log(window.a,a);//undefined  、a is not defined
    a = 10;
    console.log(window.a,a)//10 10
}
console.log(window.a,a);//10 10

结论:我们可以清晰的看到,在块级作用域中默认声明的变量,只有执行了声明代码,变量才会被挂载到全局作用域上。

让我们在看看使用var声明的变量的情况:

console.log(window.a,a);//undefined  undefined
{
  console.log(window.a,a);//undefined  、undefined
    var a = 10;
    console.log(window.a,a)//10 10
}
console.log(window.a,a);//10 10

结论:对比上一个例子,我们可以得出,使用var声明的变量会在编译阶段被提升到全局作用域上,不过它只是将声明提升,赋值操作并未提升上去。

小结

  • 在块级作用域中默认声明的变量,只有代码执行到声明语句之后,才可以进行访问,否则会报错。
  • 块级作用域中默认声明的变量会被提升到全局作用域。

块级作用域的函数声明

在块级作用域中的函数声明和变量是不同的。不多说,直接撸代码:

console.log(a);//undefined
{
    function a(){}
}

回到第一个例子,如果块级作用域里面是一个默认变量,则第一行代码会直接报错,而函数声明则会打印undefined

其实在阮一峰老师的ECMAScript6入门中关于这一点就做了明确说明:

块级作用域函数,就像预先在全局作用域中使用var声明了一个变量,且默认值为undefined

console.log(window.a,a);//undefined undefined
{
  console.log(window.a,a);//undefined function a(){}
  function a(){};
  console.log(window.a,a)//function a(){} function a(){}
}
console.log(window.a,a);//function a(){} function a(){}

在这个例子中,第一行输出大概是比较符合预期的。至于第二行window.a=undefoned,而a=function a(){} ,上面在阮一峰老师的文章中说过,声明函数a会被提升到全局作用域,且在其块级作用域中,也会被提升到顶层。而window.a为什么会为undefined呢?因为只有window.a只有等块级作用域中函数声明的定义的那行代码执行过之后,才会被映射到全局作用域。

小结

  • 块级作用域函数在编译阶段将函数声明提升到全局作用域,并且会在全局声明一个变量,值为undefined。同时,也会被提升到对应的块级作用域顶层。
  • 块级作用域函数只有定义声明函数的那行代码执行过后,才会被映射到全局作用域。

块级作用域中有同名的变量和函数声明

看🌰:

console.log(window.a,a);//undefined undefined
{
    console.log(window.a,a);//undefined function a(){}
    function a() {};
    a = 10;
    console.log(window.a,a); //function a(){}  10
};
console.log(window.a,a); //function a(){}  function a(){}

在这里,首先,块级作用域函数a的声明会被提升到全局作用域,第一行打印比较符合预期。然后在块级作用域中,由于声明函数a提升到块级作用域顶端,所以打印a = function a(){},而window.a由于并没有执行函数定义的那一行代码,所以仍然为undefined。当执行到声明函数定义的时候,就会把函数a映射到全局作用域中。当执行a = 10的时候,JS引擎会进行LHS查找,此时,声明函数已经被同时提升到全局作用域和块级作用域顶端了,由于遮蔽效果,此时查找a只会找到块级作用域内的a,并不会找到全局作用域的a,这时,a已经被定义,a = 10只会执行赋值操作,并不会进行提升。

我们再来看🌰2。它会更加抽象,准备好了没?

console.log(window.a,a);//undefined undefined
{
    console.log(window.a,a);//undefined function a(){}
       a = 10;
    function a() {};
    console.log(window.a,a); //10  10
};
console.log(window.a,a); //10 10

有了上面例子的铺垫,相信大家对前两行的打印结果不会有任何疑问了吧?

那好,我们直接进行下一步,执行a = 10,我们知道,此时,在块级作用域中函数声明已经被提升到顶层,那么此时执行a,就是相当于赋值,将函数声明a赋值为数字a,可以理解吗?如果有疑问,可以看🌰1。然后,执行到函数声明语句,此时,虽然这一行代码是函数声明语句,但是a,已经为数字10了,所以,执行function a(){}之后,a的值10就会被赋值给全局作用域上的a,所以下面打印的window.a,a都为10!!!

小结

  • 块级作用域函数只有执行函数声明语句的时候,才会重写对应的全局作用域上的同名变量。

异步编程

JavaScript异步编程

JavaScript异步编程

Promise

Promise是异步编程的一种实现方式,它比传统的解决方案更加合适、强大。所谓Promise,就是一种容器,容器里面保存着未来才会发生的事情(一般是异步操作)的结果。

Promise是一个对象,它提供一系列的API可以将异步操作的事情以同步的形式表现出来,比起”回调地狱”,它看起来更加合适、强大。它具有以下几大特点:

  • Promise的状态不受任何外界的影响。Promise的异步操作有三种状态: pending(执行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果才能够决定Promise状态的走向。
  • Promise的状态一旦改变,变不可再次变动。它的状态变化只有两种情况:pending => fulfilledpengding => rejected。状态一旦发生变化,就不再改变,称之为 resolved(已定型)。

Promise一旦新建之后,就会立即执行。而then指定的回调函数只有同步操作执行完之后才会执行。

const promise = new Promise((resolve,reject) => {

});
promise.then(() => {//成功回调
  //todo
},(error) => {//失败回调

}).catch(e => {
  //todo
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。但是一般默认不指定rejected状态的回调函数,而是使用catch

finally不管promise最终的执行结果如何,都会执行。

all:方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

举个🌰:

var p1 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p1'
    })
  }, 3000)
});
var p2 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p2'
    })
  }, 1000)
});
var p3 = new Promise(resolve => {
  setTimeout(() => {
    resolve({
        msg: 'p3'
    })
  }, 500)
});
let p = Promise.all([p1,p2,p3]);
p.then(([res1,res2,res3]) => {
  console.log(res1);
  console.log(res2);
  console.log(res3);
}).catch(e => {

});
//执行结果:p1 p2 p3

p的状态由p1,p2,p3决定:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

race:该方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

let p = Promise.race([p1, p2, p3]);
p.then((res) => {
    console.log(res);
});

只要只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

allSettled:该方法由ES2020引入。Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

anyPromise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案

resolvePromise.resolve()将现有对象转为 Promise 对象。

  • 参数是一个Promise实例:那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  • 参数是一个 thenable 实例:thenable对象指的是具有then方法的对象,比如下面这个对象。:

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

  • 参数不是具有then方法的对象,或根本就不是对象:如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved
const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

rejectPromise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

Generator/yield

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征:

1、function关键字与函数名之间有一个星号;

2、函数体内部使用yield表达式,定义不同的内部状态

Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
let hwg = helloWorldGenerator();
hwg.next();//{ value:hello,done:false }
hwg.next();//{ value:world,done:false }
hwg.next();//{ value:ending,done:true }
hwg.next();//{ value:undefined,done:true }

总结:调用Generator函数,会返回一个遍历器对象,它是函数内部的指针,以后每调用一次next函数,就会返回一个带有value和done属性的对象。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。它是Generator的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

async函数对 Generator 函数的改进,体现在以下几点。

  • 内置执行器。

Generator 函数的执行必须靠执行器。而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

  • 返回值是 Promise

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

async函数返回一个 Promise 对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

举个🌰:

function timeFn(s){
  //Promise 新建后就会立即执行。
  return new Promise((resolve,reject) => {
    setTimeout(() => {
      resolve({
        msg:'hello'
      })
    },s)
  })
}
async function fun(time){
  //Promise的写法
  // timeFn(time).then(res => {
  //     console.log(res);
  // })

  //async await的写法
  let str = await timeFn(time);
  console.log(str);
}
fun(3000)

程序会在3s之后打印{msg:’hello’}

await:正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

await命令只能用在async函数之中,如果用在普通函数,就会报错。

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
//spawn 自动执行器

JavaScript块级作用域的默认变量和声明函数 置顶

块级作用域的变量声明和函数声明

最近在一个讨论群看到一个面试题:

{
  a = 10;
  function a(){};
  console.log(a)//10
};
console.log(a);//10

what?不应该是:function a(){} 、function a(){} 吗?

最后一位牛人给出了他的理解,这里本人只是作出验证,加深理解。

接下来的环境默认都是chrome环境,支持ES6。

下文提出的默认变量指的是没有使用 var、let、const定义的变量。

块级作用域的默认变量

我们先看这样一个🌰:

console.log(a);//ReferenceError: a is not defined
{
    a = 10;
    console.log(a)//10
}
console.log(a);//10

可能有些人会像我一样感到诧异:为什么第一个a打印出来的不是undefined?a = 10默认不是 var a = 10吗?应该会提升到全局作用域的顶端的啊。

那好,让我们把window.a打印出来看看:

console.log(window.a);//undefined
{
  console.log(window.a,a);//undefined  、a is not defined
    a = 10;
    console.log(window.a,a)//10 10
}
console.log(window.a,a);//10 10

结论:我们可以清晰的看到,在块级作用域中默认声明的变量,只有执行了声明代码,变量才会被挂载到全局作用域上。

让我们在看看使用var声明的变量的情况:

console.log(window.a,a);//undefined  undefined
{
  console.log(window.a,a);//undefined  、undefined
    var a = 10;
    console.log(window.a,a)//10 10
}
console.log(window.a,a);//10 10

结论:对比上一个例子,我们可以得出,使用var声明的变量会在编译阶段被提升到全局作用域上,不过它只是将声明提升,赋值操作并未提升上去。

小结

  • 在块级作用域中默认声明的变量,只有代码执行到声明语句之后,才可以进行访问,否则会报错。
  • 块级作用域中默认声明的变量会被提升到全局作用域。

块级作用域的函数声明

在块级作用域中的函数声明和变量是不同的。不多说,直接撸代码:

console.log(a);//undefined
{
    function a(){}
}

回到第一个例子,如果块级作用域里面是一个默认变量,则第一行代码会直接报错,而函数声明则会打印undefined

其实在阮一峰老师的ECMAScript6入门中关于这一点就做了明确说明:

块级作用域函数,就像预先在全局作用域中使用var声明了一个变量,且默认值为undefined

console.log(window.a,a);//undefined undefined
{
  console.log(window.a,a);//undefined function a(){}
  function a(){};
  console.log(window.a,a)//function a(){} function a(){}
}
console.log(window.a,a);//function a(){} function a(){}

在这个例子中,第一行输出大概是比较符合预期的。至于第二行window.a=undefoned,而a=function a(){} ,上面在阮一峰老师的文章中说过,声明函数a会被提升到全局作用域,且在其块级作用域中,也会被提升到顶层。而window.a为什么会为undefined呢?因为只有window.a只有等块级作用域中函数声明的定义的那行代码执行过之后,才会被映射到全局作用域。

小结

  • 块级作用域函数在编译阶段将函数声明提升到全局作用域,并且会在全局声明一个变量,值为undefined。同时,也会被提升到对应的块级作用域顶层。
  • 块级作用域函数只有定义声明函数的那行代码执行过后,才会被映射到全局作用域。

块级作用域中有同名的变量和函数声明

看🌰:

console.log(window.a,a);//undefined undefined
{
    console.log(window.a,a);//undefined function a(){}
    function a() {};
    a = 10;
    console.log(window.a,a); //function a(){}  10
};
console.log(window.a,a); //function a(){}  function a(){}

在这里,首先,块级作用域函数a的声明会被提升到全局作用域,第一行打印比较符合预期。然后在块级作用域中,由于声明函数a提升到块级作用域顶端,所以打印a = function a(){},而window.a由于并没有执行函数定义的那一行代码,所以仍然为undefined。当执行到声明函数定义的时候,就会把函数a映射到全局作用域中。当执行a = 10的时候,JS引擎会进行LHS查找,此时,声明函数已经被同时提升到全局作用域和块级作用域顶端了,由于遮蔽效果,此时查找a只会找到块级作用域内的a,并不会找到全局作用域的a,这时,a已经被定义,a = 10只会执行赋值操作,并不会进行提升。

我们再来看🌰2。它会更加抽象,准备好了没?

console.log(window.a,a);//undefined undefined
{
    console.log(window.a,a);//undefined function a(){}
       a = 10;
    function a() {};
    console.log(window.a,a); //10  10
};
console.log(window.a,a); //10 10

有了上面例子的铺垫,相信大家对前两行的打印结果不会有任何疑问了吧?

那好,我们直接进行下一步,执行a = 10,我们知道,此时,在块级作用域中函数声明已经被提升到顶层,那么此时执行a,就是相当于赋值,将函数声明a赋值为数字a,可以理解吗?如果有疑问,可以看🌰1。然后,执行到函数声明语句,此时,虽然这一行代码是函数声明语句,但是a,已经为数字10了,所以,执行function a(){}之后,a的值10就会被赋值给全局作用域上的a,所以下面打印的window.a,a都为10!!!

小结

  • 块级作用域函数只有执行函数声明语句的时候,才会重写对应的全局作用域上的同名变量。