Ynl0ZQ

Ynl0ZQ 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Ynl0ZQ 收藏了文章 · 10月2日

webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化

图片描述

webpack,打包所有的资源

不知道不觉,webpack已经偷偷更新到4.34版本了,本人决定,这是今年最后一篇写webpack的文章,除非它更新到版本5,本人今年剩下的时间都会放在Golang和二进制数据操作以及后端的生态上

在看本文前,假设你对webpack有一定了解,如果不了解,可以看看我之前的手写ReactVue脚手架的文章

在此对webpack的性能优化进行几点声明:
  • 在部分极度复杂的环境下,需要双package.json文件,即实行三次打包
  • 在代码分割时,低于18K的文件没必要单独打包成一个chunk,http请求次数过多反而影响性能
  • prerenderPWA互斥,这个问题暂时没有解决
  • babel缓存编译缓存的是索引,即hash值,非常吃内存,每次开发完记得清理内存
  • babel-polyfill按需加载在某些非常复杂的场景下比较适合
  • prefetch,preload对首屏优化提升是明显
  • 代码分割不管什么技术栈,一定要做,不然就是垃圾项目
  • 多线程编译对构建速度提升也很明显
  • 代码分割配合PWA+预渲染+preload是首屏优化的巅峰,但是pwa无法缓存预渲染的html文件

本文的webpack主要针对React技术栈,实现功能如下:

  • 开发模式热更新
  • 识别JSX文件
  • 识别class组件
  • 代码混淆压缩,防止反编译代码,加密代码
  • 配置alias别名,简化import的长字段
  • 同构直出,SSR的热调试(基于Node做中间件)
  • 实现javaScripttree shaking 摇树优化 删除掉无用代码
  • 实现CSStree shaking
  • 识别 async / await 和 箭头函数
  • react-hot-loader记录react页面留存状态state
  • PWA功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用
  • preload 预加载资源 prefetch按需请求资源
  • CSS模块化,不怕命名冲突
  • 小图片的base64处理
  • 文件后缀省掉jsx js json
  • 实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
  • 支持less sass stylus等预处理
  • code spliting 优化首屏加载时间 不让一个文件体积过大
  • 加入dns-prefetchpreload预请求必要的资源,加快首屏渲染(京东策略)
  • 加入prerender,极大加快首屏渲染速度
  • 提取公共代码,打包成一个chunk
  • 每个chunk有对应的chunkhash,每个文件有对应的contenthash,方便浏览器区别缓存
  • 图片压缩
  • CSS压缩
  • 增加CSS前缀 兼容各种浏览器
  • 对于各种不同文件打包输出指定文件夹下
  • 缓存babel的编译结果,加快编译速度
  • 每个入口文件,对应一个chunk,打包出来后对应一个文件 也是code spliting
  • 删除HTML文件的注释等无用内容
  • 每次编译删除旧的打包代码
  • CSS文件单独抽取出来
  • 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
  • 等等....

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

webpack打包原理

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码
  • 这些都是webpack的一些基础知识,对于理解webpack的工作机制很有帮助。

舒适的开发体验,有助于提高我们的开发效率,优化开发体验也至关重要

  • 组件热刷新、CSS热刷新
  • 自从webpack推出热刷新后,前端开发者在开环境下体验大幅提高。
  • 没有热刷新能力,我们修改一个组件后

clipboard.png

  • 加入热刷新后

clipboard.png

主要看一下React技术栈,如何在构建中接入热刷新

  • 无论什么技术栈,都需要在dev模式下加上 webpack.HotModuleReplacementPlugin插件
  devServer: {
        contentBase: '../build',
        open: true,
        port: 5000,
        hot: true
    },


注:也可以使用react-hot-loader来实现,具体参考官方文档

在开发模式下也要代码分割,加快打开页面速度

optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
            minSize: 10000, // 提高缓存利用率,这需要在http2/spdy
            maxSize: 0,//没有限制
            minChunks: 3,// 共享最少的chunk数,使用次数超过这个值才会被提取
            maxAsyncRequests: 5,//最多的异步chunk数
            maxInitialRequests: 5,// 最多的同步chunks数
            automaticNameDelimiter: '~',// 多页面共用chunk命名分隔符
            name: true,
            cacheGroups: {// 声明的公共chunk
            vendor: {
            // 过滤需要打入的模块
            test: module => {
            if (module.resource) {
            const include = [/[\\/]node_modules[\\/]/].every(reg => {
            return reg.test(module.resource);
            });
            const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {
            return reg.test(module.resource);
            });
            return include && !exclude;
            }
            return false;
            },
            name: 'vendor',
            priority: 50,// 确定模块打入的优先级
            reuseExistingChunk: true,// 使用复用已经存在的模块
            },
            react: {
            test({ resource }) {
            return /[\\/]node_modules[\\/](react|redux)/.test(resource);
            },
            name: 'react',
            priority: 20,
            reuseExistingChunk: true,
            },
            antd: {
            test: /[\\/]node_modules[\\/]antd/,
            name: 'antd',
            priority: 15,
            reuseExistingChunk: true,
            },
            },
        }
    }

简要解释上面这段配置

  • 将node_modules共用部分打入vendor.js bundle中;
  • 将react全家桶打入react.js bundle中;
  • 如果项目依赖了antd,那么将antd打入单独的bundle中;(其实不用这样,可以看我下面的babel配置,性能更高)
  • 最后剩下的业务模块超过3次引用的公共模块,将自动提取公共块
注意 上面的配置只是为了给大家看,其实这样配置代码分割,性能更高
optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
                     }
}

react-hot-loader记录react页面留存状态state

yarn add react-hot-loader
 
 
// 在入口文件里这样写
 
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";-------------------1、首先引入AppContainre
import { BrowserRouter } from "react-router-dom";
import Router from "./router";
 
/*初始化*/
renderWithHotReload(Router);-------------------2、初始化
 
/*热更新*/
if (module.hot) {-------------------3、热更新操作
  module.hot.accept("./router/index.js", () => {
    const Router = require("./router/index.js").default;
    renderWithHotReload(Router);
  });
}
 
 
function renderWithHotReload(Router) {-------------------4、定义渲染函数
  ReactDOM.render(
    <AppContainer>
      <BrowserRouter>
        <Router />
      </BrowserRouter>
    </AppContainer>,
    document.getElementById("app")
  );
}
然后你再刷新试试

React的按需加载,附带代码分割功能 ,每个按需加载的组件打包后都会被单独分割成一个文件


        import React from 'react'
        import loadable from 'react-loadable'
        import Loading from '../loading' 
        const LoadableComponent = loadable({
            loader: () => import('../Test/index.jsx'),
            loading: Loading,
        });
        class Assets extends React.Component {
            render() {
                return (
                    <div>
                        <div>这即将按需加载</div>
                        <LoadableComponent />
                    </div>
                )
            }
        }
        
        export default Assets

* 加入html-loader识别html文件

    {
    test: /\.(html)$/,
    loader: 'html-loader'
    }

配置别名

    resolve: {
    modules: [
    path.resolve(__dirname, 'src'), 
    path.resolve(__dirname,'node_modules'),
    ],
    alias: {
    components: path.resolve(__dirname, '/src/components'),
    },
    }
 

加入eslint-loader

    {
    enforce:'pre',
    test:/\.js$/,
    exclude:/node_modules/,
    include:resolve(__dirname,'/src/js'),
    loader:'eslint-loader'
    }

resolve解析配置,为了为了给所有文件后缀省掉 js jsx json,加入配置

resolve: {
    extensions: [".js", ".json", ".jsx"]
}

加入HTML文件压缩,自动将入门的js文件注入html中,优化HTML文件

 new HtmlWebpackPlugin({
            template: './public/index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
            }
        }),

SSR同构直出热调试

  • , 采用 webpack watch+nodemon 结合的模式实现对SSR热调试的支持。node 服务需要的html/js通过webpack插件动态输出,当nodemon检测到变化后将自动重启,html文件中的静态资源全部替换为dev模式下的资源,并保持socket连接自动更新页面。
  • 实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。下面是SSR热调试的流程图:

clipboard.png

加入 babel-loader 还有 解析JSX ES6语法的 babel preset

  • @babel/preset-react解析 jsx语法
  • @babel/preset-env解析es6语法
  • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加载,附带code spliting功能
  • ["import", { libraryName: "antd-mobile", style: true }], Antd-mobile的按需加载
{
                            loader: 'babel-loader',
                            options: {   //jsx语法
                                presets: ["@babel/preset-react",
                                    //tree shaking 按需加载babel-polifill
                                    ["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
                                plugins: [
                                    //支持import 懒加载 
                                    "@babel/plugin-syntax-dynamic-import",
                                    //andt-mobile按需加载  true是less,如果不用less style的值可以写'css' 
                                    ["import", { libraryName: "antd-mobile", style: true }],
                                    //识别class组件
                                    ["@babel/plugin-proposal-class-properties", { "loose": true }],
                                ],
                                cacheDirectory: true
                            },
                        }
特别提示,如果电脑性能不高,不建议开启babel缓存索引,非常吃内存,记得每次开发完了清理内存

加入thread-loader,在babel首次编译后开启多线程

    const os = require('os')
    {
            loader: 'thread-loader',
            options: {
                workers: os.cpus().length   
                     }
    }

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

CSStree shaking

const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
    // 清除无用 css
    new PurifyCSS({
      paths: glob.sync([
        // 要做 CSS Tree Shaking 的路径文件
        path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html 文件进行 tree shaking
        path.resolve(__dirname, './src/*.js')
      ])
    })
]

 

对小图片进行base64处理,减少http请求数量,并对输出的文件统一打包处理

{
                    test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                    loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',

                    }
                }, {
                    exclude: /\.(js|json|less|css|jsx)$/,
                    loader: 'file-loader',
                    options: {
                        outputPath: 'media/',
                        name: '[name].[hash].[ext]'
                    }
                }
                ]
            }]
    },
    

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

加入压缩css的插件

    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    new OptimizeCssAssetsWebpackPlugin({
                cssProcessPluginOptions:{
                    preset:['default',{discardComments: {removeAll:true} }]
                }
            }),

加入每次打包输出文件清空上次打包文件的插件

    const CleanWebpackPlugin = require('clean-webpack-plugin')
    
    new CleanWebpackPlugin()

加入图片压缩

{
                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                
                use:[
                    {loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',
                        outputPath:'/img'
                    }},
                    {
                        loader: 'img-loader',
                        options: {
                          plugins: [
                            require('imagemin-gifsicle')({
                              interlaced: false
                            }),
                            require('imagemin-mozjpeg')({
                              progressive: true,
                              arithmetic: false
                            }),
                            require('imagemin-pngquant')({
                              floyd: 0.5,
                              speed: 2
                            }),
                            require('imagemin-svgo')({
                              plugins: [
                                { removeTitle: true },
                                { convertPathData: false }
                              ]
                            })
                          ]
                        }
                      }
                ]
                
                

            }

加入代码混淆,反编译

var JavaScriptObfuscator = require('webpack-obfuscator');

// ...

// webpack plugins array
plugins: [
    new JavaScriptObfuscator ({
      rotateUnicodeArray: true
  }, ['excluded_bundle_name.js'])
],

加入 PWA的插件 , WorkboxPlugin

  • pwa这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')


    new WorkboxPlugin.GenerateSW({ 
                clientsClaim: true, //让浏览器立即servece worker被接管
                skipWaiting: true,  // 更新sw文件后,立即插队到最前面 
                importWorkboxFrom: 'local',
                include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
            }),
        

加入预加载preload

new PreloadWebpackPlugin({
            rel: 'preload',
            as(entry) {
                if (/\.css$/.test(entry)) return 'style';
                if (/\.woff$/.test(entry)) return 'font';
                if (/\.png$/.test(entry)) return 'image';
                return 'script';
            },
            include: 'allChunks'
            //include: ['app']
        }),

加入预渲染


const PrerenderSPAPlugin = require('prerender-spa-plugin')

new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),

我这套webpack配置,无论多复杂的环境,都是可以搞定的
  • webpack真的非常非常重要,如果用不好,就永远是个初级前端
  • 只要webpack不更新到5,以后就不出webpack的文章了
  • webpack4大结局,谢谢
  • 以后会出一些偏向跨平台技术,原生javascriptTSGolang等内容的文章
查看原文

Ynl0ZQ 赞了文章 · 10月2日

webpack4大结局:加入腾讯IM配置策略,实现前端工程化环境极致优化

图片描述

webpack,打包所有的资源

不知道不觉,webpack已经偷偷更新到4.34版本了,本人决定,这是今年最后一篇写webpack的文章,除非它更新到版本5,本人今年剩下的时间都会放在Golang和二进制数据操作以及后端的生态上

在看本文前,假设你对webpack有一定了解,如果不了解,可以看看我之前的手写ReactVue脚手架的文章

在此对webpack的性能优化进行几点声明:
  • 在部分极度复杂的环境下,需要双package.json文件,即实行三次打包
  • 在代码分割时,低于18K的文件没必要单独打包成一个chunk,http请求次数过多反而影响性能
  • prerenderPWA互斥,这个问题暂时没有解决
  • babel缓存编译缓存的是索引,即hash值,非常吃内存,每次开发完记得清理内存
  • babel-polyfill按需加载在某些非常复杂的场景下比较适合
  • prefetch,preload对首屏优化提升是明显
  • 代码分割不管什么技术栈,一定要做,不然就是垃圾项目
  • 多线程编译对构建速度提升也很明显
  • 代码分割配合PWA+预渲染+preload是首屏优化的巅峰,但是pwa无法缓存预渲染的html文件

本文的webpack主要针对React技术栈,实现功能如下:

  • 开发模式热更新
  • 识别JSX文件
  • 识别class组件
  • 代码混淆压缩,防止反编译代码,加密代码
  • 配置alias别名,简化import的长字段
  • 同构直出,SSR的热调试(基于Node做中间件)
  • 实现javaScripttree shaking 摇树优化 删除掉无用代码
  • 实现CSStree shaking
  • 识别 async / await 和 箭头函数
  • react-hot-loader记录react页面留存状态state
  • PWA功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用
  • preload 预加载资源 prefetch按需请求资源
  • CSS模块化,不怕命名冲突
  • 小图片的base64处理
  • 文件后缀省掉jsx js json
  • 实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
  • 支持less sass stylus等预处理
  • code spliting 优化首屏加载时间 不让一个文件体积过大
  • 加入dns-prefetchpreload预请求必要的资源,加快首屏渲染(京东策略)
  • 加入prerender,极大加快首屏渲染速度
  • 提取公共代码,打包成一个chunk
  • 每个chunk有对应的chunkhash,每个文件有对应的contenthash,方便浏览器区别缓存
  • 图片压缩
  • CSS压缩
  • 增加CSS前缀 兼容各种浏览器
  • 对于各种不同文件打包输出指定文件夹下
  • 缓存babel的编译结果,加快编译速度
  • 每个入口文件,对应一个chunk,打包出来后对应一个文件 也是code spliting
  • 删除HTML文件的注释等无用内容
  • 每次编译删除旧的打包代码
  • CSS文件单独抽取出来
  • 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
  • 等等....

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

webpack打包原理

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码
  • 这些都是webpack的一些基础知识,对于理解webpack的工作机制很有帮助。

舒适的开发体验,有助于提高我们的开发效率,优化开发体验也至关重要

  • 组件热刷新、CSS热刷新
  • 自从webpack推出热刷新后,前端开发者在开环境下体验大幅提高。
  • 没有热刷新能力,我们修改一个组件后

clipboard.png

  • 加入热刷新后

clipboard.png

主要看一下React技术栈,如何在构建中接入热刷新

  • 无论什么技术栈,都需要在dev模式下加上 webpack.HotModuleReplacementPlugin插件
  devServer: {
        contentBase: '../build',
        open: true,
        port: 5000,
        hot: true
    },


注:也可以使用react-hot-loader来实现,具体参考官方文档

在开发模式下也要代码分割,加快打开页面速度

optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
            minSize: 10000, // 提高缓存利用率,这需要在http2/spdy
            maxSize: 0,//没有限制
            minChunks: 3,// 共享最少的chunk数,使用次数超过这个值才会被提取
            maxAsyncRequests: 5,//最多的异步chunk数
            maxInitialRequests: 5,// 最多的同步chunks数
            automaticNameDelimiter: '~',// 多页面共用chunk命名分隔符
            name: true,
            cacheGroups: {// 声明的公共chunk
            vendor: {
            // 过滤需要打入的模块
            test: module => {
            if (module.resource) {
            const include = [/[\\/]node_modules[\\/]/].every(reg => {
            return reg.test(module.resource);
            });
            const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {
            return reg.test(module.resource);
            });
            return include && !exclude;
            }
            return false;
            },
            name: 'vendor',
            priority: 50,// 确定模块打入的优先级
            reuseExistingChunk: true,// 使用复用已经存在的模块
            },
            react: {
            test({ resource }) {
            return /[\\/]node_modules[\\/](react|redux)/.test(resource);
            },
            name: 'react',
            priority: 20,
            reuseExistingChunk: true,
            },
            antd: {
            test: /[\\/]node_modules[\\/]antd/,
            name: 'antd',
            priority: 15,
            reuseExistingChunk: true,
            },
            },
        }
    }

简要解释上面这段配置

  • 将node_modules共用部分打入vendor.js bundle中;
  • 将react全家桶打入react.js bundle中;
  • 如果项目依赖了antd,那么将antd打入单独的bundle中;(其实不用这样,可以看我下面的babel配置,性能更高)
  • 最后剩下的业务模块超过3次引用的公共模块,将自动提取公共块
注意 上面的配置只是为了给大家看,其实这样配置代码分割,性能更高
optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
                     }
}

react-hot-loader记录react页面留存状态state

yarn add react-hot-loader
 
 
// 在入口文件里这样写
 
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";-------------------1、首先引入AppContainre
import { BrowserRouter } from "react-router-dom";
import Router from "./router";
 
/*初始化*/
renderWithHotReload(Router);-------------------2、初始化
 
/*热更新*/
if (module.hot) {-------------------3、热更新操作
  module.hot.accept("./router/index.js", () => {
    const Router = require("./router/index.js").default;
    renderWithHotReload(Router);
  });
}
 
 
function renderWithHotReload(Router) {-------------------4、定义渲染函数
  ReactDOM.render(
    <AppContainer>
      <BrowserRouter>
        <Router />
      </BrowserRouter>
    </AppContainer>,
    document.getElementById("app")
  );
}
然后你再刷新试试

React的按需加载,附带代码分割功能 ,每个按需加载的组件打包后都会被单独分割成一个文件


        import React from 'react'
        import loadable from 'react-loadable'
        import Loading from '../loading' 
        const LoadableComponent = loadable({
            loader: () => import('../Test/index.jsx'),
            loading: Loading,
        });
        class Assets extends React.Component {
            render() {
                return (
                    <div>
                        <div>这即将按需加载</div>
                        <LoadableComponent />
                    </div>
                )
            }
        }
        
        export default Assets

* 加入html-loader识别html文件

    {
    test: /\.(html)$/,
    loader: 'html-loader'
    }

配置别名

    resolve: {
    modules: [
    path.resolve(__dirname, 'src'), 
    path.resolve(__dirname,'node_modules'),
    ],
    alias: {
    components: path.resolve(__dirname, '/src/components'),
    },
    }
 

加入eslint-loader

    {
    enforce:'pre',
    test:/\.js$/,
    exclude:/node_modules/,
    include:resolve(__dirname,'/src/js'),
    loader:'eslint-loader'
    }

resolve解析配置,为了为了给所有文件后缀省掉 js jsx json,加入配置

resolve: {
    extensions: [".js", ".json", ".jsx"]
}

加入HTML文件压缩,自动将入门的js文件注入html中,优化HTML文件

 new HtmlWebpackPlugin({
            template: './public/index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
            }
        }),

SSR同构直出热调试

  • , 采用 webpack watch+nodemon 结合的模式实现对SSR热调试的支持。node 服务需要的html/js通过webpack插件动态输出,当nodemon检测到变化后将自动重启,html文件中的静态资源全部替换为dev模式下的资源,并保持socket连接自动更新页面。
  • 实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。下面是SSR热调试的流程图:

clipboard.png

加入 babel-loader 还有 解析JSX ES6语法的 babel preset

  • @babel/preset-react解析 jsx语法
  • @babel/preset-env解析es6语法
  • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加载,附带code spliting功能
  • ["import", { libraryName: "antd-mobile", style: true }], Antd-mobile的按需加载
{
                            loader: 'babel-loader',
                            options: {   //jsx语法
                                presets: ["@babel/preset-react",
                                    //tree shaking 按需加载babel-polifill
                                    ["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
                                plugins: [
                                    //支持import 懒加载 
                                    "@babel/plugin-syntax-dynamic-import",
                                    //andt-mobile按需加载  true是less,如果不用less style的值可以写'css' 
                                    ["import", { libraryName: "antd-mobile", style: true }],
                                    //识别class组件
                                    ["@babel/plugin-proposal-class-properties", { "loose": true }],
                                ],
                                cacheDirectory: true
                            },
                        }
特别提示,如果电脑性能不高,不建议开启babel缓存索引,非常吃内存,记得每次开发完了清理内存

加入thread-loader,在babel首次编译后开启多线程

    const os = require('os')
    {
            loader: 'thread-loader',
            options: {
                workers: os.cpus().length   
                     }
    }

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

CSStree shaking

const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
    // 清除无用 css
    new PurifyCSS({
      paths: glob.sync([
        // 要做 CSS Tree Shaking 的路径文件
        path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html 文件进行 tree shaking
        path.resolve(__dirname, './src/*.js')
      ])
    })
]

 

对小图片进行base64处理,减少http请求数量,并对输出的文件统一打包处理

{
                    test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                    loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',

                    }
                }, {
                    exclude: /\.(js|json|less|css|jsx)$/,
                    loader: 'file-loader',
                    options: {
                        outputPath: 'media/',
                        name: '[name].[hash].[ext]'
                    }
                }
                ]
            }]
    },
    

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

加入压缩css的插件

    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    new OptimizeCssAssetsWebpackPlugin({
                cssProcessPluginOptions:{
                    preset:['default',{discardComments: {removeAll:true} }]
                }
            }),

加入每次打包输出文件清空上次打包文件的插件

    const CleanWebpackPlugin = require('clean-webpack-plugin')
    
    new CleanWebpackPlugin()

加入图片压缩

{
                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                
                use:[
                    {loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',
                        outputPath:'/img'
                    }},
                    {
                        loader: 'img-loader',
                        options: {
                          plugins: [
                            require('imagemin-gifsicle')({
                              interlaced: false
                            }),
                            require('imagemin-mozjpeg')({
                              progressive: true,
                              arithmetic: false
                            }),
                            require('imagemin-pngquant')({
                              floyd: 0.5,
                              speed: 2
                            }),
                            require('imagemin-svgo')({
                              plugins: [
                                { removeTitle: true },
                                { convertPathData: false }
                              ]
                            })
                          ]
                        }
                      }
                ]
                
                

            }

加入代码混淆,反编译

var JavaScriptObfuscator = require('webpack-obfuscator');

// ...

// webpack plugins array
plugins: [
    new JavaScriptObfuscator ({
      rotateUnicodeArray: true
  }, ['excluded_bundle_name.js'])
],

加入 PWA的插件 , WorkboxPlugin

  • pwa这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')


    new WorkboxPlugin.GenerateSW({ 
                clientsClaim: true, //让浏览器立即servece worker被接管
                skipWaiting: true,  // 更新sw文件后,立即插队到最前面 
                importWorkboxFrom: 'local',
                include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
            }),
        

加入预加载preload

new PreloadWebpackPlugin({
            rel: 'preload',
            as(entry) {
                if (/\.css$/.test(entry)) return 'style';
                if (/\.woff$/.test(entry)) return 'font';
                if (/\.png$/.test(entry)) return 'image';
                return 'script';
            },
            include: 'allChunks'
            //include: ['app']
        }),

加入预渲染


const PrerenderSPAPlugin = require('prerender-spa-plugin')

new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),

我这套webpack配置,无论多复杂的环境,都是可以搞定的
  • webpack真的非常非常重要,如果用不好,就永远是个初级前端
  • 只要webpack不更新到5,以后就不出webpack的文章了
  • webpack4大结局,谢谢
  • 以后会出一些偏向跨平台技术,原生javascriptTSGolang等内容的文章
查看原文

赞 59 收藏 43 评论 9

Ynl0ZQ 回答了问题 · 9月29日

react在编译的时候为什么不能加div?

刚刚也遇到这个问题,你这个文件是.js吧?要改成.jsx才能正常编译

关注 4 回答 3

Ynl0ZQ 收藏了文章 · 8月3日

JavaScript栈内存和堆内存

栈内存和堆内存

JavaScript中的变量分为基本类型和引用类型

基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问

引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

结合代码与图来理解

let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量b存在于栈中,{ x: 10 }作为对象存在于堆中
let c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3]作为对象存在于堆中

当我们要访问堆内存中的引用数据类型时

  1. 从栈中获取该对象的地址引用
  2. 再从堆内存中取得我们需要的数据

基本类型发生复制行为

let a = 20;
let b = a;
b = 30;
console.log(a); // 20

结合下面图进行理解:

在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是相互独立互不影响的

引用类型发生复制行为

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
  1. 引用类型的复制,同样为新的变量b分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针
  2. 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个
  3. 因此改变b.x时,a.x也发生了变化,这就是引用类型的特性
  4. 结合下图理解

总结

查看原文

Ynl0ZQ 收藏了文章 · 8月3日

你可能不需要在 JavaScript 使用 switch 语句!

作者:Valentino Gagliardi
译者:前端小智
来源:valentinog
点赞再看,微信搜索 【大迁世界】 关注这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

没有 switch 就没有复杂的代码块

switch很方便:给定一个表达式,我们可以检查它是否与一堆case子句中的其他表达式匹配。 考虑以下示例:

const name = "Juliana";

switch (name) {
  case "Juliana":
    console.log("She's Juliana");
    break;
  case "Tom":
    console.log("She's not Juliana");
    break;
}

name“Juliana”时,我们将打印一条消息,并立即中断退出该块。 在switch函数内部时,直接在 case 块使用 return,就可以省略break

当没有匹配项时,可以使用 default 选项:

const name = "Kris";

switch (name) {
  case "Juliana":
    console.log("She's Juliana");
    break;
  case "Tom":
    console.log("She's not Juliana");
    break;
  default:
    console.log("Sorry, no match");
}

switch在 Redux reducers 中也大量使用(尽管Redux Toolkit简化了样板),以避免产生大量的if。 考虑以下示例:

const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";

const authState = {
  token: "",
  error: "",
};

function authReducer(state = authState, action) {
  switch (action.type) {
    case LOGIN_SUCCESS:
      return { ...state, token: action.payload };
    case LOGIN_FAILED:
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

这有什么问题吗?几乎没有。但是有没有更好的选择呢?

从 Python 获得的启示

来自 Telmo 的这条 Tweet引起了我的注意。 他展示了两种“switch”风格,其中一种非常接近Python中的模式。

Python 没有开关,它给我们一个更好的替代方法。 首先让我们将代码从 JavaScript 移植到Python:

LOGIN_SUCCESS = "LOGIN_SUCCESS"
LOGIN_FAILED = "LOGIN_FAILED"

auth_state = {"token": "", "error": ""}


def auth_reducer(state=auth_state, action={}):
    mapping = {
        LOGIN_SUCCESS: {**state, "token": action["payload"]},
        LOGIN_FAILED: {**state, "error": action["payload"]},
    }

    return mapping.get(action["type"], state)

在 Python 中,我们可以使用字典来模拟switchdict.get() 可以用来表示 switchdefault 语句。

当访问不存在的key时,Python 会触发一个 KeyError 错误:

>>> my_dict = {
    "name": "John", 
    "city": "Rome", 
    "age": 44
    }

>>> my_dict["not_here"]

# Output: KeyError: 'not_here'

.get()方法是一种更安全方法,因为它不会引发错误,并且可以为不存在的key指定默认值:

>>> my_dict = {
    "name": "John", 
    "city": "Rome", 
    "age": 44
    }

>>> my_dict.get("not_here", "not found")

# Output: 'not found'

因此,Pytho n中的这一行:

    return mapping.get(action["type"], state)

等价于 JavaScript中的:

function authReducer(state = authState, action) {
  ...
    default:
      return state;
  ...
}

使用字典的方式替换 switch

再次思考前面的示例:

const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";

const authState = {
  token: "",
  error: "",
};

function authReducer(state = authState, action) {
  switch (action.type) {
    case LOGIN_SUCCESS:
      return { ...state, token: action.payload };
    case LOGIN_FAILED:
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

如果不使用 switch 我们可以这样做:

function authReducer(state = authState, action) {
  const mapping = {
    [LOGIN_SUCCESS]: { ...state, token: action.payload },
    [LOGIN_FAILED]: { ...state, error: action.payload }
  };

  return mapping[action.type] || state;
}

这里我们使用 ES6 中的计算属性,此处,mapping的属性是根据两个常量即时计算的:LOGIN_SUCCESSLOGIN_FAILED
属性对应的值,我们这里使用的是对象解构,这里 ES9((ECMAScript 2018)) 出来的。

const mapping = {
  [LOGIN_SUCCESS]: { ...state, token: action.payload },
  [LOGIN_FAILED]: { ...state, error: action.payload }
}

你如何看待这种方法?它对 switch 来说可能还能一些限制,但对于 reducer 来说可能是一种更好的方案。

但是,此代码的性能如何?

性能怎么样?

switch 的性能优于字典的写法。我们可以使用下面的事例测试一下:

console.time("sample");
for (let i = 0; i < 2000000; i++) {
  const nextState = authReducer(authState, {
    type: LOGIN_SUCCESS,
    payload: "some_token"
  });
}
console.timeEnd("sample");

测量它们十次左右,

for t in {1..10}; do node switch.js >> switch.txt;done
for t in {1..10}; do node map.js >> map.txt;done

clipboard.png

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


原文:https://codeburst.io/alternat...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


交流

文章每周持续更新,可以微信搜索 【大迁世界 】 第一时间阅读,回复 【福利】 有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

Ynl0ZQ 收藏了文章 · 8月3日

Vue中使用装饰器,我是认真的

产品上线事繁多,测试产品催不离。
休问Bug剩多少,眼圈如漆身如泥。

作为一个曾经的Java coder, 当我第一次看到js里面的装饰器(Decorator)的时候,就马上想到了Java中的注解,当然在实际原理和功能上面,Java的注解和js的装饰器还是有很大差别的。本文题目是Vue中使用装饰器,我是认真的,但本文将从装饰器的概念开发聊起,一起来看看吧。

通过本文内容,你将学到以下内容:

  1. 了解什么是装饰器
  2. 在方法使用装饰器
  3. class中使用装饰器
  4. Vue中使用装饰器
本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注 === 大厂offer

什么是装饰器

装饰器是ES2016提出来的一个提案,当前处于Stage 2阶段,关于装饰器的体验,可以点击 https://github.com/tc39/proposal-decorators查看详情。装饰器是一种与类相关的语法糖,用来包装或者修改类或者类的方法的行为,其实装饰器就是设计模式中装饰者模式的一种实现方式。不过前面说的这些概念太干了,我们用人话来翻译一下,举一个例子。

在日常开发写bug过程中,我们经常会用到防抖和节流,比如像下面这样

class MyClass {
  follow = debounce(function() {
    console.log('我是子君,关注我哦')
  }, 100)
}

const myClass = new MyClass()
// 多次调用只会输出一次
myClass.follow()
myClass.follow()

上面是一个防抖的例子,我们通过debounce函数将另一个函数包起来,实现了防抖的功能,这时候再有另一个需求,比如希望在调用follow函数前后各打印一段日志,这时候我们还可以再开发一个log函数,然后继续将follow包装起来

/**
 * 最外层是防抖,否则log会被调用多次
 */
class MyClass {
  follow = debounce(
    log(function() {
      console.log('我是子君,关注我哦')
    }),
    100
  )
}

上面代码中的debouncelog两个函数,本质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为发生了变化,而js中的装饰器的原理就是这样的,我们使用装饰器对上面的代码进行改造

class MyClass {
  @debounce(100)
  @log
  follow() {
    console.log('我是子君,关注我哦')
  }
}

装饰器的形式就是 @ + 函数名,如果有参数的话,后面的括号里面可以传参

在方法上使用装饰器

装饰器可以应用到class上或者class里面的属性上面,但一般情况下,应用到class属性上面的场景会比较多一些,比如像上面我们说的log,debounce等等,都一般会应用到类属性上面,接下来我们一起来具体看一下如何实现一个装饰器,并应用到类上面。在实现装饰器之前,我们需要先了解一下属性描述符

了解一下属性描述符

在我们定义一个对象里面的属性的时候,其实这个属性上面是有许多属性描述符的,这些描述符标明了这个属性能不能修改,能不能枚举,能不能删除等等,同时ECMAScript将这些属性描述符分为两类,分别是数据属性和访问器属性,并且数据属性与访问器属性是不能共存的。

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性包含了四个描述符,分别是

  1. configurable

    表示能不能通过delete删除属性,能否修改属性的其他描述符特性,或者能否将数据属性修改为访问器属性。当我们通过let obj = {name: ''}声明一个对象的时候,这个对象里面所有的属性的configurable描述符的值都是true

  2. enumerable

    表示能不能通过for in或者Object.keys等方式获取到属性,我们一般声明的对象里面这个描述符的值是true,但是对于class类里面的属性来说,这个值是false

  3. writable

    表示能否修改属性的数据值,通过将这个修改为false,可以实现属性只读的效果。

  4. value

    表示当前属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个位置。

访问器属性

访问器属性不包含数据值,他们包含了gettersetter两个函数,同时configurableenumerable是数据属性与访问器属性共有的两个描述符。

  1. getter

    在读取属性的时候调用这个函数,默认这个函数为undefined

  2. setter

    在写入属性值的时候调用这个函数,默认这个函数为undefined

了解了这六个描述符之后,你可能会有几个疑问: 我如何去定义修改这些属性描述符?这些属性描述符与今天的文章主题有什么关系?接下来是揭晓答案的时候了。

使用Object.defineProperty

了解过vue2.0双向绑定原理的同学一定知道,Vue的双向绑定就是通过使用Object.defineProperty去定义数据属性的gettersetter方法来实现的,比如下面有一个对象

let obj = {
  name: '子君',
  officialAccounts: '前端有的玩'
}

我希望这个对象里面的用户名是不能被修改的,用Object.defineProperty该如何定义呢?

Object.defineProperty(obj,'name', {
  // 设置writable 是 false, 这个属性将不能被修改
  writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)

通过Object.defineProperty可以去定义或者修改对象属性的属性描述符,但是因为数据属性与访问器属性是互斥的,所以一次只能修改其中的一类,这一点需要注意。

定义一个防抖装饰器

装饰器本质上依然是一个函数,不过这个函数的参数是固定的,如下是防抖装饰器的代码

/**
*@param wait 延迟时长
*/
function debounce(wait) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait)
  }
}
// 使用方式
class MyClass {
  @debounce(100)
  follow() {
    console.log('我是子君,我的公众号是 【前端有的玩】,关注有惊喜哦')
  }
}

我们逐行去分析一下代码

  1. 首先我们定义了一个 debounce函数,同时有一个参数wait,这个函数对应的就是在下面调用装饰器时使用的@debounce(100)
  2. debounce函数返回了一个新的函数,这个函数即装饰器的核心,这个函数有三个参数,下面逐一分析

    1. target: 这个类属性函数是在谁上面挂载的,如上例对应的是MyClass
    2. name: 这个类属性函数的名称,对应上面的follow
    3. descriptor: 这个就是我们前面说的属性描述符,通过直接descriptor上面的属性,即可实现属性只读,数据重写等功能
  3. 然后第三行 descriptor.value = debounce(descriptor.value, wait), 前面我们已经了解到,属性描述符上面的value对应的是这个属性的值,所以我们通过重写这个属性,将其用debounce函数包装起来,这样在函数调用follow时实际调用的是包装后的函数

通过上面的三步,我们就实现了类属性上面可使用的装饰器,同时将其应用到了类属性上面

class上使用装饰器

装饰器不仅可以应用到类属性上面,还可以直接应用到类上面,比如我希望可以实现一个类似Vue混入那样的功能,给一个类混入一些方法属性,应该如何去做呢?

// 这个是要混入的对象
const methods = {
  logger() {
    console.log('记录日志')
  }
}

// 这个是一个登陆登出类
class Login{
  login() {}
  logout() {}
}

如何将上面的methods混入到Login中,首先我们先实现一个类装饰器

function mixins(obj) {
  return function (target) {
    Object.assign(target.prototype, obj)  
  }
}

// 然后通过装饰器混入
@mixins(methods)
class Login{
  login() {}
  logout() {}
}

这样就实现了类装饰器。对于类装饰器,只有一个参数,即target,对应的就是这个类本身。

了解完装饰器,我们接下来看一下如何在Vue中使用装饰器。

Vue中使用装饰器

使用ts开发Vue的同学一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器,方便大家开发的时候使用,当然本文的中点不是这个插件。其实如果我们的项目没有使用ts,也是可以使用装饰器的,怎么用呢?

配置基础环境

除了一些老的项目,我们现在一般新建Vue项目的时候,都会选择使用脚手架vue-cli3/4来新建,这时候新建的项目已经默认支持了装饰器,不需要再配置太多额外的东西,如果你的项目使用了eslint,那么需要给eslint配置以下内容。

  parserOptions: {
    ecmaFeatures:{
      // 支持装饰器
      legacyDecorators: true
    }
  }

使用装饰器

虽然Vue的组件,我们一般书写的时候export出去的是一个对象,但是这个并不影响我们直接在组件中使用装饰器,比如就拿上例中的log举例。

function log() {
  /**
   * @param target 对应 methods 这个对象
   * @param name 对应属性方法的名称
   * @param descriptor 对应属性方法的修饰符
   */
  return function(target, name, descriptor) {
    console.log(target, name, descriptor)
    const fn = descriptor.value
    descriptor.value = function(...rest) {
      console.log(`这是调用方法【${name}】前打印的日志`)
      fn.call(this, ...rest)
      console.log(`这是调用方法【${name}】后打印的日志`)
    }
  }
}

export default {
  created() {
    this.getData()
  },
  methods: {
    @log()
    getData() {
      console.log('获取数据')
    }
  }
}

看了上面的代码,是不是发现在Vue中使用装饰器还是很简单的,和在class的属性上面使用的方式一模一样,但有一点需要注意,在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods

除了在methods上面可以使用装饰器之外,你也可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。

一些常用的装饰器

下面小编罗列了几个小编在项目中常用的几个装饰器,方便大家使用

1. 函数节流与防抖

函数节流与防抖应用场景是比较广的,一般使用时候会通过throttledebounce方法对要调用的函数进行包装,现在就可以使用上文说的内容将这两个函数封装成装饰器, 防抖节流使用的是lodash提供的方法,大家也可以自行实现节流防抖函数哦

import { throttle, debounce } from 'lodash'
/**
 * 函数节流装饰器
 * @param {number} wait 节流的毫秒
 * @param {Object} options 节流选项对象
 * [options.leading=true] (boolean): 指定调用在节流开始前。
 * [options.trailing=true] (boolean): 指定调用在节流结束后。
 */
export const throttle =  function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = throttle(descriptor.value, wait, options)
  }
}

/**
 * 函数防抖装饰器
 * @param {number} wait 需要延迟的毫秒数。
 * @param {Object} options 选项对象
 * [options.leading=false] (boolean): 指定在延迟开始前调用。
 * [options.maxWait] (number): 设置 func 允许被延迟的最大值。
 * [options.trailing=true] (boolean): 指定在延迟结束后调用。
 */
export const debounce = function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait, options)
  }
}

封装完之后,在组件中使用

import {debounce} from '@/decorator'

export default {
  methods:{
    @debounce(100)
    resize(){}
  }
}

2. loading

在加载数据的时候,为了个用户一个友好的提示,同时防止用户继续操作,一般会在请求前显示一个loading,然后在请求结束之后关掉loading,一般写法如下

export default {
  methods:{
    async getData() {
      const loading = Toast.loading()
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }finally{
        loading.clear()
      }  
    }
  }
}

我们可以把上面的loading的逻辑使用装饰器重新封装,如下代码

import { Toast } from 'vant'

/**
 * loading 装饰器
 * @param {*} message 提示信息
 * @param {function} errorFn 异常处理逻辑
 */
export const loading =  function(message = '加载中...', errorFn = function() {}) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    descriptor.value = async function(...rest) {
      const loading = Toast.loading({
        message: message,
        forbidClick: true
      })
      try {
        return await fn.call(this, ...rest)
      } catch (error) {
        // 在调用失败,且用户自定义失败的回调函数时,则执行
        errorFn && errorFn.call(this, error, ...rest)
        console.error(error)
      } finally {
        loading.clear()
      }
    }
  }
}

然后改造上面的组件代码

export default {
  methods:{
    @loading('加载中')
    async getData() {
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }  
    }
  }
}

3. 确认框

当你点击删除按钮的时候,一般都需要弹出一个提示框让用户确认是否删除,这时候常规写法可能是这样的

import { Dialog } from 'vant'

export default {
  methods: {
    deleteData() {
      Dialog.confirm({
        title: '提示',
        message: '确定要删除数据,此操作不可回退。'
      }).then(() => {
        console.log('在这里做删除操作')
      })
    }
  }
}

我们可以把上面确认的过程提出来做成装饰器,如下代码

import { Dialog } from 'vant'

/**
 * 确认提示框装饰器
 * @param {*} message 提示信息
 * @param {*} title 标题
 * @param {*} cancelFn 取消回调函数
 */
export function confirm(
  message = '确定要删除数据,此操作不可回退。',
  title = '提示',
  cancelFn = function() {}
) {
  return function(target, name, descriptor) {
    const originFn = descriptor.value
    descriptor.value = async function(...rest) {
      try {
        await Dialog.confirm({
          message,
          title: title
        })
        originFn.apply(this, rest)
      } catch (error) {
        cancelFn && cancelFn(error)
      }
    }
  }
}

然后再使用确认框的时候,就可以这样使用了

export default {
  methods: {
    // 可以不传参,使用默认参数
    @confirm()
    deleteData() {
      console.log('在这里做删除操作')
    }
  }
}

是不是瞬间简单多了,当然还可以继续封装很多很多的装饰器,因为文章内容有限,暂时提供这三个。

装饰器组合使用

在上面我们将类属性上面使用装饰器的时候,说道装饰器可以组合使用,在Vue组件上面使用也是一样的,比如我们希望在确认删除之后,调用接口时候出现loading,就可以这样写(一定要注意顺序)

export default {
  methods: {
    @confirm()
    @loading()
    async deleteData() {
      await delete()
    }
  }
}
本节定义的装饰器,均已应用到这个项目中 https://github.com/snowzijun/vue-vant-base, 这是一个基于Vant开发的开箱即用移动端框架,你只需要fork下来,无需做任何配置就可以直接进行业务开发,欢迎使用,喜欢麻烦给一个star

我是子君,今天就写这么多,本文首发于【前端有的玩】,这是一个专注于前端技术,前端面试相关的公众号,同时关注之后即刻拉你加入前端交流群,我们一起聊前端,欢迎关注。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

Ynl0ZQ 赞了文章 · 8月3日

Vue中使用装饰器,我是认真的

产品上线事繁多,测试产品催不离。
休问Bug剩多少,眼圈如漆身如泥。

作为一个曾经的Java coder, 当我第一次看到js里面的装饰器(Decorator)的时候,就马上想到了Java中的注解,当然在实际原理和功能上面,Java的注解和js的装饰器还是有很大差别的。本文题目是Vue中使用装饰器,我是认真的,但本文将从装饰器的概念开发聊起,一起来看看吧。

通过本文内容,你将学到以下内容:

  1. 了解什么是装饰器
  2. 在方法使用装饰器
  3. class中使用装饰器
  4. Vue中使用装饰器
本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注 === 大厂offer

什么是装饰器

装饰器是ES2016提出来的一个提案,当前处于Stage 2阶段,关于装饰器的体验,可以点击 https://github.com/tc39/proposal-decorators查看详情。装饰器是一种与类相关的语法糖,用来包装或者修改类或者类的方法的行为,其实装饰器就是设计模式中装饰者模式的一种实现方式。不过前面说的这些概念太干了,我们用人话来翻译一下,举一个例子。

在日常开发写bug过程中,我们经常会用到防抖和节流,比如像下面这样

class MyClass {
  follow = debounce(function() {
    console.log('我是子君,关注我哦')
  }, 100)
}

const myClass = new MyClass()
// 多次调用只会输出一次
myClass.follow()
myClass.follow()

上面是一个防抖的例子,我们通过debounce函数将另一个函数包起来,实现了防抖的功能,这时候再有另一个需求,比如希望在调用follow函数前后各打印一段日志,这时候我们还可以再开发一个log函数,然后继续将follow包装起来

/**
 * 最外层是防抖,否则log会被调用多次
 */
class MyClass {
  follow = debounce(
    log(function() {
      console.log('我是子君,关注我哦')
    }),
    100
  )
}

上面代码中的debouncelog两个函数,本质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为发生了变化,而js中的装饰器的原理就是这样的,我们使用装饰器对上面的代码进行改造

class MyClass {
  @debounce(100)
  @log
  follow() {
    console.log('我是子君,关注我哦')
  }
}

装饰器的形式就是 @ + 函数名,如果有参数的话,后面的括号里面可以传参

在方法上使用装饰器

装饰器可以应用到class上或者class里面的属性上面,但一般情况下,应用到class属性上面的场景会比较多一些,比如像上面我们说的log,debounce等等,都一般会应用到类属性上面,接下来我们一起来具体看一下如何实现一个装饰器,并应用到类上面。在实现装饰器之前,我们需要先了解一下属性描述符

了解一下属性描述符

在我们定义一个对象里面的属性的时候,其实这个属性上面是有许多属性描述符的,这些描述符标明了这个属性能不能修改,能不能枚举,能不能删除等等,同时ECMAScript将这些属性描述符分为两类,分别是数据属性和访问器属性,并且数据属性与访问器属性是不能共存的。

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性包含了四个描述符,分别是

  1. configurable

    表示能不能通过delete删除属性,能否修改属性的其他描述符特性,或者能否将数据属性修改为访问器属性。当我们通过let obj = {name: ''}声明一个对象的时候,这个对象里面所有的属性的configurable描述符的值都是true

  2. enumerable

    表示能不能通过for in或者Object.keys等方式获取到属性,我们一般声明的对象里面这个描述符的值是true,但是对于class类里面的属性来说,这个值是false

  3. writable

    表示能否修改属性的数据值,通过将这个修改为false,可以实现属性只读的效果。

  4. value

    表示当前属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个位置。

访问器属性

访问器属性不包含数据值,他们包含了gettersetter两个函数,同时configurableenumerable是数据属性与访问器属性共有的两个描述符。

  1. getter

    在读取属性的时候调用这个函数,默认这个函数为undefined

  2. setter

    在写入属性值的时候调用这个函数,默认这个函数为undefined

了解了这六个描述符之后,你可能会有几个疑问: 我如何去定义修改这些属性描述符?这些属性描述符与今天的文章主题有什么关系?接下来是揭晓答案的时候了。

使用Object.defineProperty

了解过vue2.0双向绑定原理的同学一定知道,Vue的双向绑定就是通过使用Object.defineProperty去定义数据属性的gettersetter方法来实现的,比如下面有一个对象

let obj = {
  name: '子君',
  officialAccounts: '前端有的玩'
}

我希望这个对象里面的用户名是不能被修改的,用Object.defineProperty该如何定义呢?

Object.defineProperty(obj,'name', {
  // 设置writable 是 false, 这个属性将不能被修改
  writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)

通过Object.defineProperty可以去定义或者修改对象属性的属性描述符,但是因为数据属性与访问器属性是互斥的,所以一次只能修改其中的一类,这一点需要注意。

定义一个防抖装饰器

装饰器本质上依然是一个函数,不过这个函数的参数是固定的,如下是防抖装饰器的代码

/**
*@param wait 延迟时长
*/
function debounce(wait) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait)
  }
}
// 使用方式
class MyClass {
  @debounce(100)
  follow() {
    console.log('我是子君,我的公众号是 【前端有的玩】,关注有惊喜哦')
  }
}

我们逐行去分析一下代码

  1. 首先我们定义了一个 debounce函数,同时有一个参数wait,这个函数对应的就是在下面调用装饰器时使用的@debounce(100)
  2. debounce函数返回了一个新的函数,这个函数即装饰器的核心,这个函数有三个参数,下面逐一分析

    1. target: 这个类属性函数是在谁上面挂载的,如上例对应的是MyClass
    2. name: 这个类属性函数的名称,对应上面的follow
    3. descriptor: 这个就是我们前面说的属性描述符,通过直接descriptor上面的属性,即可实现属性只读,数据重写等功能
  3. 然后第三行 descriptor.value = debounce(descriptor.value, wait), 前面我们已经了解到,属性描述符上面的value对应的是这个属性的值,所以我们通过重写这个属性,将其用debounce函数包装起来,这样在函数调用follow时实际调用的是包装后的函数

通过上面的三步,我们就实现了类属性上面可使用的装饰器,同时将其应用到了类属性上面

class上使用装饰器

装饰器不仅可以应用到类属性上面,还可以直接应用到类上面,比如我希望可以实现一个类似Vue混入那样的功能,给一个类混入一些方法属性,应该如何去做呢?

// 这个是要混入的对象
const methods = {
  logger() {
    console.log('记录日志')
  }
}

// 这个是一个登陆登出类
class Login{
  login() {}
  logout() {}
}

如何将上面的methods混入到Login中,首先我们先实现一个类装饰器

function mixins(obj) {
  return function (target) {
    Object.assign(target.prototype, obj)  
  }
}

// 然后通过装饰器混入
@mixins(methods)
class Login{
  login() {}
  logout() {}
}

这样就实现了类装饰器。对于类装饰器,只有一个参数,即target,对应的就是这个类本身。

了解完装饰器,我们接下来看一下如何在Vue中使用装饰器。

Vue中使用装饰器

使用ts开发Vue的同学一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器,方便大家开发的时候使用,当然本文的中点不是这个插件。其实如果我们的项目没有使用ts,也是可以使用装饰器的,怎么用呢?

配置基础环境

除了一些老的项目,我们现在一般新建Vue项目的时候,都会选择使用脚手架vue-cli3/4来新建,这时候新建的项目已经默认支持了装饰器,不需要再配置太多额外的东西,如果你的项目使用了eslint,那么需要给eslint配置以下内容。

  parserOptions: {
    ecmaFeatures:{
      // 支持装饰器
      legacyDecorators: true
    }
  }

使用装饰器

虽然Vue的组件,我们一般书写的时候export出去的是一个对象,但是这个并不影响我们直接在组件中使用装饰器,比如就拿上例中的log举例。

function log() {
  /**
   * @param target 对应 methods 这个对象
   * @param name 对应属性方法的名称
   * @param descriptor 对应属性方法的修饰符
   */
  return function(target, name, descriptor) {
    console.log(target, name, descriptor)
    const fn = descriptor.value
    descriptor.value = function(...rest) {
      console.log(`这是调用方法【${name}】前打印的日志`)
      fn.call(this, ...rest)
      console.log(`这是调用方法【${name}】后打印的日志`)
    }
  }
}

export default {
  created() {
    this.getData()
  },
  methods: {
    @log()
    getData() {
      console.log('获取数据')
    }
  }
}

看了上面的代码,是不是发现在Vue中使用装饰器还是很简单的,和在class的属性上面使用的方式一模一样,但有一点需要注意,在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods

除了在methods上面可以使用装饰器之外,你也可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。

一些常用的装饰器

下面小编罗列了几个小编在项目中常用的几个装饰器,方便大家使用

1. 函数节流与防抖

函数节流与防抖应用场景是比较广的,一般使用时候会通过throttledebounce方法对要调用的函数进行包装,现在就可以使用上文说的内容将这两个函数封装成装饰器, 防抖节流使用的是lodash提供的方法,大家也可以自行实现节流防抖函数哦

import { throttle, debounce } from 'lodash'
/**
 * 函数节流装饰器
 * @param {number} wait 节流的毫秒
 * @param {Object} options 节流选项对象
 * [options.leading=true] (boolean): 指定调用在节流开始前。
 * [options.trailing=true] (boolean): 指定调用在节流结束后。
 */
export const throttle =  function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = throttle(descriptor.value, wait, options)
  }
}

/**
 * 函数防抖装饰器
 * @param {number} wait 需要延迟的毫秒数。
 * @param {Object} options 选项对象
 * [options.leading=false] (boolean): 指定在延迟开始前调用。
 * [options.maxWait] (number): 设置 func 允许被延迟的最大值。
 * [options.trailing=true] (boolean): 指定在延迟结束后调用。
 */
export const debounce = function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait, options)
  }
}

封装完之后,在组件中使用

import {debounce} from '@/decorator'

export default {
  methods:{
    @debounce(100)
    resize(){}
  }
}

2. loading

在加载数据的时候,为了个用户一个友好的提示,同时防止用户继续操作,一般会在请求前显示一个loading,然后在请求结束之后关掉loading,一般写法如下

export default {
  methods:{
    async getData() {
      const loading = Toast.loading()
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }finally{
        loading.clear()
      }  
    }
  }
}

我们可以把上面的loading的逻辑使用装饰器重新封装,如下代码

import { Toast } from 'vant'

/**
 * loading 装饰器
 * @param {*} message 提示信息
 * @param {function} errorFn 异常处理逻辑
 */
export const loading =  function(message = '加载中...', errorFn = function() {}) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    descriptor.value = async function(...rest) {
      const loading = Toast.loading({
        message: message,
        forbidClick: true
      })
      try {
        return await fn.call(this, ...rest)
      } catch (error) {
        // 在调用失败,且用户自定义失败的回调函数时,则执行
        errorFn && errorFn.call(this, error, ...rest)
        console.error(error)
      } finally {
        loading.clear()
      }
    }
  }
}

然后改造上面的组件代码

export default {
  methods:{
    @loading('加载中')
    async getData() {
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }  
    }
  }
}

3. 确认框

当你点击删除按钮的时候,一般都需要弹出一个提示框让用户确认是否删除,这时候常规写法可能是这样的

import { Dialog } from 'vant'

export default {
  methods: {
    deleteData() {
      Dialog.confirm({
        title: '提示',
        message: '确定要删除数据,此操作不可回退。'
      }).then(() => {
        console.log('在这里做删除操作')
      })
    }
  }
}

我们可以把上面确认的过程提出来做成装饰器,如下代码

import { Dialog } from 'vant'

/**
 * 确认提示框装饰器
 * @param {*} message 提示信息
 * @param {*} title 标题
 * @param {*} cancelFn 取消回调函数
 */
export function confirm(
  message = '确定要删除数据,此操作不可回退。',
  title = '提示',
  cancelFn = function() {}
) {
  return function(target, name, descriptor) {
    const originFn = descriptor.value
    descriptor.value = async function(...rest) {
      try {
        await Dialog.confirm({
          message,
          title: title
        })
        originFn.apply(this, rest)
      } catch (error) {
        cancelFn && cancelFn(error)
      }
    }
  }
}

然后再使用确认框的时候,就可以这样使用了

export default {
  methods: {
    // 可以不传参,使用默认参数
    @confirm()
    deleteData() {
      console.log('在这里做删除操作')
    }
  }
}

是不是瞬间简单多了,当然还可以继续封装很多很多的装饰器,因为文章内容有限,暂时提供这三个。

装饰器组合使用

在上面我们将类属性上面使用装饰器的时候,说道装饰器可以组合使用,在Vue组件上面使用也是一样的,比如我们希望在确认删除之后,调用接口时候出现loading,就可以这样写(一定要注意顺序)

export default {
  methods: {
    @confirm()
    @loading()
    async deleteData() {
      await delete()
    }
  }
}
本节定义的装饰器,均已应用到这个项目中 https://github.com/snowzijun/vue-vant-base, 这是一个基于Vant开发的开箱即用移动端框架,你只需要fork下来,无需做任何配置就可以直接进行业务开发,欢迎使用,喜欢麻烦给一个star

我是子君,今天就写这么多,本文首发于【前端有的玩】,这是一个专注于前端技术,前端面试相关的公众号,同时关注之后即刻拉你加入前端交流群,我们一起聊前端,欢迎关注。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 24 收藏 17 评论 0

Ynl0ZQ 收藏了文章 · 8月3日

15 张精美动图全面讲解 CORS

前言:

本文翻译自 Lydia Hallie 小姐姐写的 ✋🏼🔥 CS Visualized: CORS,她用了大量的动图去解释 CORS 这个概念,国内还没有人翻译本文,所以我在原文的理解上翻译了本文并修改了一些错误,希望能帮到大家。

觉得翻译的不错一定要点赞哦,谢谢你,这对我真的很重要! 🌟

注:原文的动图均为 keynote 制作


前端开发中,我们经常要使用其他站点的数据。前端显示这些数据之前,必须向服务器发出请求以获取该数据。

假设我们正在访问 https://api.mywebsite.com 这个站点,点击按钮向 https://api.mywebsite.com/users 发送请求,获取网站上的一些用户信息:

⚠️:这里原作者有个笔误,把 https://api.mywebsite.com 误写为 https://www.mywebsite.com 了,图中也有这个错误,读者要注意一下不要被误导

从结果上看表现非常完美,我们向服务器发送请求,服务器返回了我们需要的 JSON 数据,前端也正常的渲染出了结果。

下面我们换一个网站试试。用 https://www.anotherwebsite.com 这个网站向 https://api.website.com/users 发送请求:

问题来了,我们请求同样的接口网站,但是这次浏览器给我们抛出一个 Error。

刚刚浏览器抛出的就是 CORS Error,下面让我们分析一下为什么会产生这种 Error,以及这个 Error 的确切含义是什么。

1.同源策略

浏览器网络请求时,有一个同源策略的机制。即默认情况下,使用 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源。

比如说, https://www.mywebsite.com 请求 https://www.mywebsite.com/page 是完全没有问题的。但是当资源位于不同协议子域端口的站点时,这个请求就是跨域的。

目前来看,同源策略会让三种行为受限:

  • Cookie、LocalStorage 和 IndexDB 访问受限
  • 无法操作跨域 DOM(常见于 iframe)
  • Javascript 发起的 XHR 和 Fetch 请求受限


那么,为什么会存在同源策略呢?

我们做个假设,如果不存在同源策略,你无意中点击了七大姑在微信上给你发的一篇养生文章链接。其实这个网页是个钓鱼网站,访问链接后就把你重定向到一个嵌入了 iframe 的攻击网站,这个 iframe 会自动加载银行网站,并通过 cookies 登录你的账户。

登陆成功后,这个钓鱼网站还可以控制 iframe 的 DOM,通过一系列骚操作把你卡里的钱转走。

这是一个非常严重的安全漏洞,我们不希望自己在互联网的内容被随便访问,更不要说这种涉及到钱的网站了。

同源策略可以帮助我们解决这个安全问题,这个策略确保我们只能访问同一站点的资源。

在这种情况下,https://www.evilwebsite.com 尝试跨站访问 https://www.bank.com 的资源,同源策略就会阻止这个操作,让钓鱼网站无法访问银行网站的数据。

说了这么多,同源策略和 CORS 又有什么关系?

2.浏览器 CORS

出于安全原因,浏览器限制从脚本内发起的跨域 HTTP 请求。 例如 XHR 和 Fetch 就遵循同源策略。这意味着使用 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源。

日常的业务开发中,我们会经常访问跨域资源,为了安全的请求跨域资源,浏览器使用一种称为 CORS 的机制。

CORS 的全名是 Cross-Origin Resource Sharing,即跨域资源共享。尽管默认情况下浏览器禁止我们访问跨域资源,但是我们可以利用 CORS 放宽这种限制,在保证安全性的前提下访问跨域资源。

浏览器可以利用 CORS 机制,放行符合规范的跨域访问,阻止不合规范的跨域访问。浏览器内部是怎么做的呢?我们下面就来分析一下。

Web 程序发出跨域请求后,浏览器会自动向我们的 HTTP header 添加一个额外的请求头字段:OriginOrigin 标记了请求的站点来源:

GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com // <- 浏览器自己加的

为了使浏览器允许访问跨域资源, 服务器返回的 response 还需要加一些响应头字段,这些字段将显式表明此服务器是否允许这个跨域请求。

3.服务端 CORS

作为服务器开发人员,我们可以通过在 HTTP 响应中添加额外的响应头字段 Access-Control-* 来表明是否允许跨域请求。根据这些 CORS 响应头字段,浏览器可以允许一些被同源策略限制的跨源响应。

虽然有好几个 CORS 响应头字段,但有一个字段是必加的,那就是 Access-Control-Allow-Origin。这个头字段的值指定了哪些站点被允许跨域访问资源。

1️⃣ 如果我们有服务器的开发权限,我们可以给 https://www.mywebsite.com 加上访问权限:将该域添加到 Access-Control-Allow-Origin 中。

这个响应头字段现在被添加到服务器发回给客户端的 response header 中。这个字段添加后,如果我们从 https://www.mywebsite.com 发送跨域请求,同源策略将不再限制 https://api.mywebsite.com 站点返回的资源。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mywebsite.com
Date: Fri, 11 Oct 2019 15:47 GM
Content-Length: 29
Content-Type: application/json
Server: Apache

{user: [{...}]}

2️⃣ 收到服务器返回的 response 后,浏览器中的 CORS 机制会检查 Access-Control-Allow-Origin 的值是否等于 request 中 Origin 的值。

在这个例子中,request 的 Originhttps://www.mywebsite.com,这和 response 中 Access-Control-Allow-Origin 的值是一样的:

3️⃣ 浏览器校验通过,前端成功地接收到跨域资源。


那么,当我们试图从一个没有在 Access-Control-Allow-Origin 中列出的网站跨域访问这些资源会发生什么呢?

如上图所示,从 https://www.anotherwebsite.com 跨域访问 https://api.mywebsite.com 资源,浏览器抛出一个 CORS Error,经过上面的讲解,我们可以读懂这个报错信息了:

The 'Access-Control-Allow-Origin' header has a value
 'https://www.mywebsite.com' that is not equal 
to the supplied origin. 

在这种情况下,Origin 的值是 https://www.anotherwebsite.com。然而,服务器在 Access-Control-Allow-Origin 响应头字段中没有标记这个站点,浏览器 CORS 机制就阻止了这个响应,我们无法在我们的代码中获取响应数据。

CORS 还允许我们添加通配符 * 作为允许的外域,这意味着该资源可以被任意外域访问,所以要注意这种特殊情况

Access-Control-Allow-Origin 是 CORS 机制提供的众多头字段之一。服务器开发人员还可以通过其它头字段扩展服务器的 CORS 策略,以允许/禁止某些请求。

另一个常见的响应头字段是 Access-Control-Allow-Methods。其指明了跨域请求所允许使用的 HTTP 方法。

在上图的案例中,只有GETPOSTPUT 方法被允许跨域访问资源。其他 HTTP 方法,例如 PATCHDELETE 都会被阻止。

如果您想知道其它的 CORS 响应头字段是什么以及它们的用途,可以查看此列表

说到PUTPATCHDELETE 这几个 HTTP 方法,CORS 处理这些方法时还有些不同。这些非简单请求会触发 CORS 的预检请求。

4.预检请求

CORS 有两种类型的请求:一种是简单请求(simple request),一种是预检请求(preflight request)。一个跨域请求到底是简单的的还是预检的,取决于一些 request header。

当请求是 GETPOST 方法并且没有任何自定义 Header 字段时,一般来说就是个简单请求。除此之外的任何请求,诸如 PUTPATCHDELETE 方法,将会产生预检。

如果你想知道一个请求必须满足哪些要求才能成为简单请求,可以查看 MDN 简单请求相关的文档

说了这么多,「预检请求」到底是什么意思?下面我们就来探讨一下。


1️⃣ 在发送实际请求之前,客户端会先使用 OPTIONS 方法发起一个预检请求,预检请求的 Access-Control-Request-* 中包含有关我们将要处理的实际请求的信息:

OPTIONS https://api.mywebsite.com/user/1 HTTP/1.1
Origin: https://www.mywebsite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

2️⃣ 服务器接收到预检请求后,会返回一个没有 body 的 HTTP 响应,这个响应标记了服务器允许的 HTTP 方法和 HTTP Header 字段:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.mywebsite.com
Access-Control-Request-Method: GET POST PUT
Access-Control-Request-Headers: Content-Type

3️⃣ 浏览器收到预检响应,并检查是否应允许发送实际请求。

⚠️:上图预检响应漏了 Access-Control-Allow-Headers: Content-Type

4️⃣ 如果预检响应检测通过,浏览器会将实际请求发送到服务器,然后服务器返回我们需要的资源。

如果预检响应没有检验通过,CORS 会阻止跨域访问,实际的请求永远不会被发送。预检请求是一种很好的方式,可以防止我们访问或修改那些没有启用 CORS 策略的服务器上的资源。

💡 为了减少网络往返次数,我们可以通过在 CORS 请求中添加 Access-Control-Max-Age 头字段来缓存预检响应。浏览器可以使用缓存来代替发送新的预检请求。

5.认证

XHR 或 Fetch 与 CORS 的一个有趣的特性是,我们可以基于 Cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XHR 或 Fetch 请求,浏览器不会发送身份凭证信息。

尽管 CORS 默认情况下不发送身份凭证,但我们可以通过添加 Access-Control-Allow-Credentials CORS 响应头来更改它。

如果要在跨域请求中包含 cookie 和其他授权信息,我们需要做以下操作:

  • XHR 请求中将 withCredentials 字段设置为 true
  • Fetch 请求中将 credentials 设为 include
  • 服务器把 Access-Control-Allow-Credentials: true 添加到响应头中
// 浏览器 fetch 请求
fetch('https://api.mywebsite,com.users', {
  credentials: "include"
})

// 浏览器 XHR 请求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// 服务器添加认证字段
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true

把上面的工作做好后,我们就可以在跨域请求中包含身份凭证信息了。

6.总结

CORS Error 一定程度上会让前端开发很头疼,但是遵循它的相关规定后,它可以让我们在浏览器中进行安全的跨域请求。

同源策略和 CORS 的知识点有很多,本文只讲了一些关键知识点,如果你想全面学习 CORS 的相关知识,我推荐你查阅MDN 文档W3C 规范,这些一手知识是最准确的。

推荐

这篇文章就到此结束了,如果觉得不错的话一定要点赞鼓励一下哦,祝大家学习进步,工作顺利!

如果想要学习更多非笔记式的 HTTP 知识,可以看看我之前写的旧文:

最后推荐一波我的个人号:卤蛋实验室(egglabs),会更新一些前端技术与图形学相关的文章,独创不灌水,欢迎大家关注。

查看原文

Ynl0ZQ 赞了文章 · 8月3日

15 张精美动图全面讲解 CORS

前言:

本文翻译自 Lydia Hallie 小姐姐写的 ✋🏼🔥 CS Visualized: CORS,她用了大量的动图去解释 CORS 这个概念,国内还没有人翻译本文,所以我在原文的理解上翻译了本文并修改了一些错误,希望能帮到大家。

觉得翻译的不错一定要点赞哦,谢谢你,这对我真的很重要! 🌟

注:原文的动图均为 keynote 制作


前端开发中,我们经常要使用其他站点的数据。前端显示这些数据之前,必须向服务器发出请求以获取该数据。

假设我们正在访问 https://api.mywebsite.com 这个站点,点击按钮向 https://api.mywebsite.com/users 发送请求,获取网站上的一些用户信息:

⚠️:这里原作者有个笔误,把 https://api.mywebsite.com 误写为 https://www.mywebsite.com 了,图中也有这个错误,读者要注意一下不要被误导

从结果上看表现非常完美,我们向服务器发送请求,服务器返回了我们需要的 JSON 数据,前端也正常的渲染出了结果。

下面我们换一个网站试试。用 https://www.anotherwebsite.com 这个网站向 https://api.website.com/users 发送请求:

问题来了,我们请求同样的接口网站,但是这次浏览器给我们抛出一个 Error。

刚刚浏览器抛出的就是 CORS Error,下面让我们分析一下为什么会产生这种 Error,以及这个 Error 的确切含义是什么。

1.同源策略

浏览器网络请求时,有一个同源策略的机制。即默认情况下,使用 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源。

比如说, https://www.mywebsite.com 请求 https://www.mywebsite.com/page 是完全没有问题的。但是当资源位于不同协议子域端口的站点时,这个请求就是跨域的。

目前来看,同源策略会让三种行为受限:

  • Cookie、LocalStorage 和 IndexDB 访问受限
  • 无法操作跨域 DOM(常见于 iframe)
  • Javascript 发起的 XHR 和 Fetch 请求受限


那么,为什么会存在同源策略呢?

我们做个假设,如果不存在同源策略,你无意中点击了七大姑在微信上给你发的一篇养生文章链接。其实这个网页是个钓鱼网站,访问链接后就把你重定向到一个嵌入了 iframe 的攻击网站,这个 iframe 会自动加载银行网站,并通过 cookies 登录你的账户。

登陆成功后,这个钓鱼网站还可以控制 iframe 的 DOM,通过一系列骚操作把你卡里的钱转走。

这是一个非常严重的安全漏洞,我们不希望自己在互联网的内容被随便访问,更不要说这种涉及到钱的网站了。

同源策略可以帮助我们解决这个安全问题,这个策略确保我们只能访问同一站点的资源。

在这种情况下,https://www.evilwebsite.com 尝试跨站访问 https://www.bank.com 的资源,同源策略就会阻止这个操作,让钓鱼网站无法访问银行网站的数据。

说了这么多,同源策略和 CORS 又有什么关系?

2.浏览器 CORS

出于安全原因,浏览器限制从脚本内发起的跨域 HTTP 请求。 例如 XHR 和 Fetch 就遵循同源策略。这意味着使用 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源。

日常的业务开发中,我们会经常访问跨域资源,为了安全的请求跨域资源,浏览器使用一种称为 CORS 的机制。

CORS 的全名是 Cross-Origin Resource Sharing,即跨域资源共享。尽管默认情况下浏览器禁止我们访问跨域资源,但是我们可以利用 CORS 放宽这种限制,在保证安全性的前提下访问跨域资源。

浏览器可以利用 CORS 机制,放行符合规范的跨域访问,阻止不合规范的跨域访问。浏览器内部是怎么做的呢?我们下面就来分析一下。

Web 程序发出跨域请求后,浏览器会自动向我们的 HTTP header 添加一个额外的请求头字段:OriginOrigin 标记了请求的站点来源:

GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com // <- 浏览器自己加的

为了使浏览器允许访问跨域资源, 服务器返回的 response 还需要加一些响应头字段,这些字段将显式表明此服务器是否允许这个跨域请求。

3.服务端 CORS

作为服务器开发人员,我们可以通过在 HTTP 响应中添加额外的响应头字段 Access-Control-* 来表明是否允许跨域请求。根据这些 CORS 响应头字段,浏览器可以允许一些被同源策略限制的跨源响应。

虽然有好几个 CORS 响应头字段,但有一个字段是必加的,那就是 Access-Control-Allow-Origin。这个头字段的值指定了哪些站点被允许跨域访问资源。

1️⃣ 如果我们有服务器的开发权限,我们可以给 https://www.mywebsite.com 加上访问权限:将该域添加到 Access-Control-Allow-Origin 中。

这个响应头字段现在被添加到服务器发回给客户端的 response header 中。这个字段添加后,如果我们从 https://www.mywebsite.com 发送跨域请求,同源策略将不再限制 https://api.mywebsite.com 站点返回的资源。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mywebsite.com
Date: Fri, 11 Oct 2019 15:47 GM
Content-Length: 29
Content-Type: application/json
Server: Apache

{user: [{...}]}

2️⃣ 收到服务器返回的 response 后,浏览器中的 CORS 机制会检查 Access-Control-Allow-Origin 的值是否等于 request 中 Origin 的值。

在这个例子中,request 的 Originhttps://www.mywebsite.com,这和 response 中 Access-Control-Allow-Origin 的值是一样的:

3️⃣ 浏览器校验通过,前端成功地接收到跨域资源。


那么,当我们试图从一个没有在 Access-Control-Allow-Origin 中列出的网站跨域访问这些资源会发生什么呢?

如上图所示,从 https://www.anotherwebsite.com 跨域访问 https://api.mywebsite.com 资源,浏览器抛出一个 CORS Error,经过上面的讲解,我们可以读懂这个报错信息了:

The 'Access-Control-Allow-Origin' header has a value
 'https://www.mywebsite.com' that is not equal 
to the supplied origin. 

在这种情况下,Origin 的值是 https://www.anotherwebsite.com。然而,服务器在 Access-Control-Allow-Origin 响应头字段中没有标记这个站点,浏览器 CORS 机制就阻止了这个响应,我们无法在我们的代码中获取响应数据。

CORS 还允许我们添加通配符 * 作为允许的外域,这意味着该资源可以被任意外域访问,所以要注意这种特殊情况

Access-Control-Allow-Origin 是 CORS 机制提供的众多头字段之一。服务器开发人员还可以通过其它头字段扩展服务器的 CORS 策略,以允许/禁止某些请求。

另一个常见的响应头字段是 Access-Control-Allow-Methods。其指明了跨域请求所允许使用的 HTTP 方法。

在上图的案例中,只有GETPOSTPUT 方法被允许跨域访问资源。其他 HTTP 方法,例如 PATCHDELETE 都会被阻止。

如果您想知道其它的 CORS 响应头字段是什么以及它们的用途,可以查看此列表

说到PUTPATCHDELETE 这几个 HTTP 方法,CORS 处理这些方法时还有些不同。这些非简单请求会触发 CORS 的预检请求。

4.预检请求

CORS 有两种类型的请求:一种是简单请求(simple request),一种是预检请求(preflight request)。一个跨域请求到底是简单的的还是预检的,取决于一些 request header。

当请求是 GETPOST 方法并且没有任何自定义 Header 字段时,一般来说就是个简单请求。除此之外的任何请求,诸如 PUTPATCHDELETE 方法,将会产生预检。

如果你想知道一个请求必须满足哪些要求才能成为简单请求,可以查看 MDN 简单请求相关的文档

说了这么多,「预检请求」到底是什么意思?下面我们就来探讨一下。


1️⃣ 在发送实际请求之前,客户端会先使用 OPTIONS 方法发起一个预检请求,预检请求的 Access-Control-Request-* 中包含有关我们将要处理的实际请求的信息:

OPTIONS https://api.mywebsite.com/user/1 HTTP/1.1
Origin: https://www.mywebsite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

2️⃣ 服务器接收到预检请求后,会返回一个没有 body 的 HTTP 响应,这个响应标记了服务器允许的 HTTP 方法和 HTTP Header 字段:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.mywebsite.com
Access-Control-Request-Method: GET POST PUT
Access-Control-Request-Headers: Content-Type

3️⃣ 浏览器收到预检响应,并检查是否应允许发送实际请求。

⚠️:上图预检响应漏了 Access-Control-Allow-Headers: Content-Type

4️⃣ 如果预检响应检测通过,浏览器会将实际请求发送到服务器,然后服务器返回我们需要的资源。

如果预检响应没有检验通过,CORS 会阻止跨域访问,实际的请求永远不会被发送。预检请求是一种很好的方式,可以防止我们访问或修改那些没有启用 CORS 策略的服务器上的资源。

💡 为了减少网络往返次数,我们可以通过在 CORS 请求中添加 Access-Control-Max-Age 头字段来缓存预检响应。浏览器可以使用缓存来代替发送新的预检请求。

5.认证

XHR 或 Fetch 与 CORS 的一个有趣的特性是,我们可以基于 Cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XHR 或 Fetch 请求,浏览器不会发送身份凭证信息。

尽管 CORS 默认情况下不发送身份凭证,但我们可以通过添加 Access-Control-Allow-Credentials CORS 响应头来更改它。

如果要在跨域请求中包含 cookie 和其他授权信息,我们需要做以下操作:

  • XHR 请求中将 withCredentials 字段设置为 true
  • Fetch 请求中将 credentials 设为 include
  • 服务器把 Access-Control-Allow-Credentials: true 添加到响应头中
// 浏览器 fetch 请求
fetch('https://api.mywebsite,com.users', {
  credentials: "include"
})

// 浏览器 XHR 请求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// 服务器添加认证字段
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true

把上面的工作做好后,我们就可以在跨域请求中包含身份凭证信息了。

6.总结

CORS Error 一定程度上会让前端开发很头疼,但是遵循它的相关规定后,它可以让我们在浏览器中进行安全的跨域请求。

同源策略和 CORS 的知识点有很多,本文只讲了一些关键知识点,如果你想全面学习 CORS 的相关知识,我推荐你查阅MDN 文档W3C 规范,这些一手知识是最准确的。

推荐

这篇文章就到此结束了,如果觉得不错的话一定要点赞鼓励一下哦,祝大家学习进步,工作顺利!

如果想要学习更多非笔记式的 HTTP 知识,可以看看我之前写的旧文:

最后推荐一波我的个人号:卤蛋实验室(egglabs),会更新一些前端技术与图形学相关的文章,独创不灌水,欢迎大家关注。

查看原文

赞 22 收藏 15 评论 0

Ynl0ZQ 提出了问题 · 7月28日

解决this指向的一个疑问

问题描述

《JavaScript设计模式与开发实践》P215第5行代码为什么要改变__self的this指向?它原先的this指向是什么?

相关代码

Function.prototype.before = function( beforefn ) {
    var __self = this;
    return function() {
        beforefn.apply( this, arguments );
        return __self.apply( this, arguments ); // __self原先的this指向不是document吗?为什么这里要改变this指向?
    }
}
document.getElementById = document.getElementById.before(function(){
    alert (1);
});
var button = document.getElementById( 'button' );

关注 4 回答 3

认证与成就

  • 获得 3 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-12-06
个人主页被 255 人浏览