• 【知识总结】金九银十offer拿到手软的前端面试题——webpack


    webpack工作原理过程

    使用webpack时非常简单,就是提供了一个webpack config,然后执行webpack提供的全局方法,就可以打包编译了。

    整个编译打包过程:

    1. webpack构建一个compiler
    2. compiler生成一个compilation
    3. compilation以入口文件为起点,构建一个模块依赖图module graph
    4. mudule graph分离为initial chunkasync chunkruntime chunkcommon chunk
    5. 获取各个chunk对应的template,使用generator为每个chunk的module构建内容,然后再为chunk构建内容
    6. compiler将compilation对应的assets输出到output指定位置

    构建module graph过程:

    1. 解析入口文件路径,获取到入口文件的绝对路径
    2. 为源文件构建module对象
    3. 读取源文件内容,使用loader处理
    4. 使用parser解析loader处理的内容
    5. 解析dependenciesblocks中收集的依赖,重复2-5,直到所有的依赖处理完成

    构建module的过程:

    • resolve:先解析模块的路径,得到的模块的绝对路径loaderparsergenerator
    • create:创建一个module对象
    • build:获取loader提供的方法、读取源文件内容、使用loader处理源文件内容、使用parser解析源文件内容、收集依赖、处理依赖

    chunks分类:

    • initial chunk:入口文件对应的chunk
    • async chunk:异步chunk,lazy module对应的chunk
    • runtime chunk:根据optomization.runtimeChunk:trueinitial chunk中分离出来,负责安装chunk、安装module、加载lazy chunk
    • normal chunk:通过optimization.splitChunks策略分离出来的chunks

    构建bundle:

    1. 根据chunk的类型,获取对应的template
    2. 根据output.filename构建bundle的文件名
    3. 遍历chunksmodule,使用generator为每一个module构建输出内容
    4. 根据template,结合module的构建内容,构建chunk的输出内容
    5. 最后利用node提供的文件功能生存bundle文件并输出到output指定位置

    webpack hash

    hash:

    • module hash:根据每个module的源文件内容、模块ID生成
    • chunk hash:根据chunkname、所有modulemodule hash生成
    • chunk contentHash:
      • chunk.contentHash.javascript:chunk中所有的js内容生成的hash
      • chunk.contentHash["css/mini-extract"]:chunk中所有的css内容生成的hash
    • compilation hash:根据chunk的对象的chunk hash信息生成

    [name].[hash].js 中的 hash 使用的是 compilation hash,所有的 bundle 都一样;

    模块热更新

    触发条件:

    • devServer.hot: true
    • 启动inline模式
    • 必须声明module.hot.accept(url, callback)

    热更新的工作原理:

    1. 浏览器构建webSocket对象,注册message事件
    2. 服务端监听到文件发生变化,生成更新以后的chunk文件,chunk文件中包含更新的module,然后通过webSocket通知浏览器更新
    3. 浏览器构建的webSocket对象触发message事件,会收到一个hash值和一个ok信息,然后通过动态添加script元素,加载新的chunk文件
    4. 根据module id在应用缓存中找到之前缓存的module,然后以module为基础,递归遍历module.parent属性,查找定义module.hot.acceptparent module。如果没有找到,热更新不起作用,只能通过reload页面来显示更新。在递归过程中,我们会把遇到的module id存储起来
    5. 找到定义module.hot.acceptparent module之后,根据4收集的module id,将installedModules中对应的module清除,然后根据module.hot.accept(url, callbcak) 中的url,重新安装关联的module
    6. 执行callback

    Loader

    编写一个loader

    loader就是一个node模块,它输出了一个函数。当某种资源需要用这个loader转换时,这个函数就会被调用。并且这个函数可以通过提供给它的this上下文访问Loader API。reverse-txt-loader

    解析顺序:从下到上,从右往左

    // 定义
    module.exports = function(src) {
        //src是原文件内容(abcde),下面对内容进行处理,这里是反转
        var result = src.split('').reverse().join('');
        //返回JavaScript源码,必须是String或者Buffer
        return `module.exports = '${result}'`;
    }
    //使用
    {
        test: /\.txt$/,
        loader: 'reverse-txt-loader'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    打包加速的方法

    • devtool为sourceMap较为耗时
    • 开发环境下不做无意义的操作:压缩代码、目录内容清理、计算文件hash、提取css等
    • 第三方依赖外部script引入:vue、ui组件、JQuery等
    • HotModuleReplacementPlugin:热更新增量构建
    • DllPlugin& DllReferencePlugin:动态链接库、提高打包效率、仅打包一次第三方模块、每次构建只打包业务代码
    • thread-loader、happypack:多线程编译、加快编译速度
    • noParse:不需要解析某些模块的依赖
    • babel-loader开启缓存cache
    • splitChunks:提取公共模块,将符合引用次数(minChunks)的模块打包到一起,利用浏览器缓存
    • Tree Shaking :基于es6提供的模块系统对代码进行静态分析,并在压缩阶段将代码中的死代码移除,减少代码体积

    打包体积,优化思路

    • webpack-bundle-anaylzer插件可以可视化查看webpack打包出来的各个文件体积大小,以便定位大文件,在进行体积优化
    • 提取第三方库 or 通过引用外部文件的方式引入第三方库
    • 代码压缩插件UglifyJsPlugin
    • 服务器启用Gzip压缩
    • 按需加载资源文件 require.ensure =
    • 剥离css文件,单独打包
    • 去除不必要的插件,开发环境与生产环境用不同配置文件
    • SpritesmithPlugin雪碧图,将多个小图片打包成一张,用bcakground-imagebackground-positionwidthheight控制显示部分
    • url-loader 文件大小小于设置的尺寸变成base-64编码文本,大与尺寸由file-loader拷贝到目标目录

    Tree Shaking

    背景: 项目中,有一个入口文件,相当于一棵树的主干,入口文件由很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用了其中的某些功能。通过tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。

    思路: 基于es6提供的模块系统对代码进行静态分析,并将代码中的死代码移除的一种技术。因此,利用tree shaking技术可以很方便地实现我们代码上的优化,减少代码体积。

    tree-shaking删除代码的原理: webpack基于es6提供的模块系统,对代码的依赖树进行静态分析,把import & export标记为3类

    • 所有的import标记为 /* harmony import */
    • 被使用过的export标记为/harmony export([type])/,其中[type]和webpack内部有关,可能是binding,immutable等
    • 没有被使用过的export标记为/* unused harmony export [FuncName] */,其中[FuncNam]为export的方法名,之后使用Uglifyjs进行代码精简,把没用的都删除

    为何基于es6模块实现(es6 module特点):

    • 只能作为模块顶层的语句出现
    • import的模块名只能是字符串常量
    • import binding是immutable的

    条件:

    • 首先源码必须遵循es6模块规范(import & export),如果是CommonJs规范(require)则无法使用
    • 编写的模块代码不能有副作用,如果在代码内部改变了外部的变量则不会被移除

    配置方法:

    在package.json里添加一个属性

    {
        // sideEffects如果设为false,webpack就会认为所有没用到的函数都是没副作用的,即删了也没关系。
        "sideEffects": false,
        // 设置黑名单,用于防止误删代码
        "sideEffects": [
            // 数组里列出黑名单,禁止shaking下列代码
            "@babel/polly-fill",
            "*.less",
            // 其它有副作用的模块
            "./src/some-side-effectful-file.js"
        ],
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    tree-shaking 摇掉代码中未使用的代码在生产模式下自动开启,tree-shaking并不是webpack中的某一配置选项,是一组功能搭配使用后的优化效果。

    // 在开发模式下,设置 usedExports: true ,打包时只会标记出哪些模块没有被使用,不会删除,因为可能会影响 source-map的标记位置的准确性。
    {
        mode: 'develpoment',
        optimization: {
            // 优化导出的模块
            usedExports: true
        },
    }
    // 在生产模式下默认开启 usedExports: true ,打包压缩时就会将没用到的代码移除
    {
        mode: 'production',
        //  这个属性的作用就是集中配置webpack内部的优化功能
        optimizition: {
            // 只导出外部使用的模块成员 负责标记枯树叶
            usedExports: true,
            minimize: true, // 自动压缩代码 负责摇掉枯树叶
            /**
             * webpack打包默认会将一个模块单独打包到一个闭包中
             * webpack3中新增的API 将所有模块都放在一个函数中 ,尽可能将所有模块合并在一起,
             * 提升效率,减少体积  达到作用域提升的效果
             */
            concatenateModules: true,
        },
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    使用tree-shaking的注意事项:

    • 使用es6模块语法编写代码
    • 工具类函数尽量以单独的形式输出,不要集中成一个对象或者类
    • 声明sideEffects
    • 自己在重构代码时也要注意副作用

    tree-shaking & babel使用babel-loader处理js代码会导致tree-shaking失效的原因:

    • tree-shaking使用的前提必须是es module组织的代码,也就是说交给es module处理的代码必须是esm。当我们使用babel-loader处理js代码之后就可能将esm转换成commonjs规范

    解决方法:

    收到配置preset-env的modules:false,确保不会开启自动转换的插件

    presets: [
        ['@babel/preset-env', {module: 'commonjs'}]
    ]
    
    • 1
    • 2
    • 3

    常用插件简述

    • webpack-dev-server
    • clean-webpack-plugin:编译前清理输出目录
    • CopyWebpackPlugin:复制文件
    • HotModuleReplacementPlugin:热更新
    • ProvidePlugin:全局变量设置
    • DefinePlugin:定义全局变量
    • splitChunks:提取公共模块,将符合引用次数的模块打包到一起
    • mini-css-extract-plugin:css单独打包
    • TerserPlugin:压缩代码
    • progress-bar-webpack-plugin:编译进度条
    • DIIPlugin & DIIReferencePlugin:提高打包效率,仅打包一次第三方模块
    • webpack-bundle-analyzer:可视化的查看webpack打包出来的各个文件体积大小
    • thread-loader,happypack:多进程编译,加快编译速度

    webpack配置优化

    主要从以下几个方面进行优化:

    • 缩小文件的搜索范围
    • 减少打包文件
    • 缓存
    • 多线程

    1)缩小文件的搜索范围

    loader

    include表示哪些目录中的文件需要进行babel-loader,exclude表示哪些目录文件不进行babel-loader。这是因为在引入第三方模块的时候,很多模块已经是打包好的,不需要再处理。如果不设置include/exclude就会被loader处理,增加打包时间。

    {
        rules: [{
            test: /\.js$/,
            use: {
                loader: 'babel-loader'
            },
            // exclude: /node_modules/,
            include: [path.resolve(__dirname, 'src')]
        }]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    noParse

    对于JQuery、lodash、chartjs等一些库,庞大且没用采用模块化标准,因此我们可以选择不解析它们。如果一下第三方模块没用使用AMD/CommonJs规范,可以使用noParse来标记这个模块,这样webpack在导入模块时,就不进行解析转换,可以提升webpack的构建速度。

    ⚠️noParse可以接受正则表达式 or 一个函数:

    {
        module: {
            // noParse: /jquery|lodash|chartjs/,
            noParse: function(content){
                return /jquery|lodash|chartjs/.test(content)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    resolve.modules

    modules告诉webpack去哪些目录下查找引用的恶模块,默认值[“node_modules”]。我们的代码中也会有大量的模块被其他模块依赖和引入,由于分布不固定,有的路径比较长,我们就利用modules进行优化。

    {
        resolve: {
            modules: [
                path.resolve(__dirname, "src"),
                path.resolve(__dirname, "node_modules"),
                "node_modules",
            ],
        },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    resolve.alias

    alias通过创建import或者require的别名,把原理导入模块的路径映射成一个新的导入路径。它和resolve.modules不同的是,它的作用是用别名代替前面的路径,不是省略。这样的好处激素webpack直接会去对应别名的目录查找模块,减少了搜索时间。

    {
      resolve: {
        alias: {
          '@': path.resolve(__dirname, 'src'),
        },
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    resolve.mainFields

    mainFields用来告诉webpack使用第三方模块中的哪个字段来导入模块。第三方模块中都会有一个package.json文件用来描述这个模块的一些属性,比如模块名(name)、版本号(version)、作者(auth)等,其中最重要的就是有多个特殊的字段用来告诉webpack导入文件的位置,有多个字段的原因是因为有些模块可以同时用于多个环境,而每个环境使用不同的文件。

    {
        resolve: {
            mainFields: ["main"],
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    resolve.extensions

    extensions字段用来在导入模块时,自动带入后缀尝试去匹配对应文件

    {
        resolve: {
            extensions: ['.js', '.json']
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    也就是说我们在require('./utils')时,Webpack先匹配utils.js,匹配不到再去匹配utils.json,如果还找不到就报错。

    因此extensions数组越长,或者正确后缀的文件越靠后,匹配的次数越多也就越耗时,所以需要优化:

    • extensions数组尽量少,项目中不存在的文件后缀不要列进去
    • 出现频率比较高的文件后缀优先放到最前面
    • 在代码中导入文件的时候,要尽量把后缀名带上,避免查找

    2)减少打包文件

    定位体积大的文件

    使用webpack-bundle-analyzer插件可以可视化的查看webpack打包出来的各个文件体积大小,以便我们定位大文件,进行体积优化。

    提取公共的代码模块、文件

    optimization.splitChunks中配置

    {
        optimization: {
            splitChunks: {
                cacheGroups: {
                    commons: {
                        test: /[\\/]node_modules[\\/]/,
                        name: "vendors",
                        chunks: "all"
                    }
                }
            },
            runtimeChunk: {
                name: 'manifest'
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    手机 IOS 软件 IPA 签名下载安装详情图文教程
    双非本23秋招之路-从考研跑路到某安全大厂(无实习、项目)
    【全开源】教育系统(FastAdmin+ThinkPHP+Uniapp)
    2022年湖北劳务资质办理需要准备什么资料?办理劳务需要注意哪些呢?甘建二
    Spring学习篇(三)
    我是如何从零到成为 Apache 顶级项目的 Committer
    数据结构与算法之复杂度
    【嵌入式软件C编程】主函数free子函数malloc地址的两种方式以及注意事项
    Spring 中 Bean 的作用域和生命周期
    免安装Android Studio使用adb连接手机设备或模拟机进行真机调试
  • 原文地址:https://blog.csdn.net/weixin_42224055/article/details/126249038