初中级面试题(webpack)

300 阅读12分钟

Webpack是一个现代JavaScript应用程序的静态模板打包器,当webpack处理应用程序时,会递归构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将这些模块打包成一个或多个bundle。而且,webpack仅能理解JavaScript和json文件,需要通过loader转换。

Webpack的其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。比如JavaScript文件、css、图片、json文件等等在webpack中都可以被当做模块来使用。这是webpack模块化的概念。

Webpack打包原理

Webpack只是一个打包模块的机制,只是把依赖的模块转化成可以代表这些包的静态文件。

Webpack就是识别你的 入口文件。识别你的模块依赖,来打包你的代码。至于你的代码使用的是commonjs还是amd或者是es6的import,webpack都会对其进行分析,来获取代码的依赖。

Webpack做的就是分析代码,转化代码,编译代码,输出代码。Webpack本身是一个node

的模块,所以webpack.config.js是以commonjs形式书写的。(node中的模块化是commonjs规范的)。

webpack配置入口出口

module.exports={   
    //入口文件的配置项    
    entry:{},    //出口文件的配置项   
    output:{},    //模块:例如解读CSS,图片如何转换,压缩    
    module:{},    //插件,用于生产模版和各项功能    
    plugins:[],    //配置webpack开发服务功能   
    devServer:{} 
}

工作流程 (加载 - 编译 - 输出)

1、读取配置文件,按命令 初始化 配置参数,创建 Compiler 对象;

2、调用插件的 apply 方法 挂载插件 监听,然后从入口文件开始执行编译;

3、按文件类型,调用相应的 Loader 对模块进行 编译,并在合适的时机点触发对应的事件,调用 Plugin 执行,最后再根据模块 依赖查找 到所依赖的模块,递归执行第三步;

4、将编译后的所有代码包装成一个个代码块 (Chuck), 并按依赖和配置确定 输出内容。这个步骤,仍然可以通过 Plugin 进行文件的修改;

5、最后,根据 Output 把文件内容一一写入到指定的文件夹中,完成整个过程;

总结:

  • 模块机制: webpack 自己实现了一套模拟模块的机制,将其包裹于业务代码的外部,从而提供了一套模块机制;
  • 文件编译: webpack 规定了一套编译规则,通过 Loader 和 Plugin,以管道的形式对文件字符串进行处理;

Loader

由于 Webpack 是基于 Node,因此 Webpack 其实是只能识别 js 模块,比如 css / html / 图片等类型的文件并无法加载,因此就需要一个对 不同格式文件转换器。其实 Loader 做的事,也并不难理解: 对 Webpack 传入的字符串进行按需修改。例如一个最简单的 Loader:

// html-loader/index.js
module.exports = function(htmlSource) {
    // 返回处理后的代码字符串
    // 删除 html 文件中的所有注释
    return htmlSource.replace(/<!--[\w\W]*?-->/g, '')
}

当然,实际的 Loader 不会这么简单,通常是需要将代码进行分析,构建 AST (抽象语法树) , 遍历进行定向的修改后,再重新生成新的代码字符串。如我们常用的 Babel-loader 会执行以下步骤:

  • babylon 将 ES6/ES7 代码解析成 AST
  • babel-traverse 对 AST 进行遍历转译,得到新的 AST
  • 新 AST 通过 babel-generator 转换成 ES5

Loader 特性:

  • 链式传递,按照配置时相反的顺序链式执行;
  • 基于 Node 环境,拥有 较高权限,比如文件的增删查改;
  • 可同步也可异步;

常用 Loader:

  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;
  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;
  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;
  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;
  • style-loader: 将 css 代码以<style>标签的形式插入到 html 中;
  • css-loader: 分析@importurl(),引用 css 文件与对应的资源;
  • postcss-loader: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;
  • less-loader / sass-loader: css预处理器,在 css 中新增了许多语法,提高了开发效率;

编写原则:

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

Plugin

插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。

  • 一个最简单的 plugin 是这样的:

    class Plugin{
        // 注册插件时,会调用 apply 方法
        // apply 方法接收 compiler 对象
        // 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
        apply(compiler){
            // compilation 是监听每次编译循环
            // 每次文件变化,都会生成新的 compilation 对象并触发该事件
            compiler.plugin('compilation',function(compilation) {})
        }
    }
    
  • 注册插件:

    // webpack.config.js
    module.export = {
        plugins:[
            new Plugin(options),
        ]
    }
    
  • 事件流机制:

    Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。

    • 通过链式调用,按顺序串起一个个 Loader;
    • 通过事件流机制,让 Plugin 可以插入到整个生产过程中的每个步骤中;

    Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:

    const { SyncHook } = require("tapable")
    ​
    const hook = new SyncHook(['arg'])
    ​
    // 订阅
    hook.tap('event', (arg) => {
        // 'event-hook'
        console.log(arg)
    })
    ​
    // 广播
    hook.call('event-hook')
    

    Webpack 中两个最重要的类 Compiler 与 Compilation 便是继承于 Tapable,也拥有这样的事件流机制。

    • Compiler: 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;

    • Compilation: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;

    • 区别:

      • Compiler 全局唯一,且从启动生存到结束;
      • Compilation 对应每次编译,每轮编译循环均会重新创建;
    • 常用 Plugin:

      • UglifyJsPlugin: 压缩、混淆代码;
      • CommonsChunkPlugin: 代码分割;
      • ProvidePlugin: 自动加载模块;
      • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
      • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件;
      • DefinePlugin: 定义全局变量;
      • optimize-css-assets-webpack-plugin: CSS 代码去重;
      • webpack-bundle-analyzer: 代码分析;
      • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
      • happypack: 使用多进程,加速代码构建;
      • EnvironmentPlugin: 定义环境变量;

编译优化

代码优化:

  • 无用代码消除,是许多编程语言都具有的优化手段,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码

    • 例如我们的 UglifyJs,它就会帮我们在生产环境中删除不可能被执行的代码,例如:
    var fn = function() {
        return 1;
        // 下面代码便属于 不可能执行的代码;
        // 通过 UglifyJs (Webpack4+ 已内置) 便会进行 DCE;
        var a = 1;
        return a;
    }
    
  • 摇树优化 (Tree-shaking),这是一种形象比喻。我们把打包后的代码比喻成一棵树,这里其实表示的就是,通过工具 "摇" 我们打包后的 js 代码,将没有使用到的无用代码 "摇" 下来 (删除)。即 消除那些被 引用了但未被使用 的模块代码。

    • 原理: 由于是在编译时优化,因此最基本的前提就是语法的静态分析,ES6的模块机制 提供了这种可能性。不需要运行时,便可进行代码字面上的静态分析,确定相应的依赖关系。

    • 问题: 具有副作用的函数无法被 tree-shaking。

      • 在引用一些第三方库,需要去观察其引入的代码量是不是符合预期;
      • 尽量写纯函数,减少函数的副作用;
      • 可使用 webpack-deep-scope-plugin,可以进行作用域分析,减少此类情况的发生,但仍需要注意;
  • code-spliting: 代码分割 技术,将代码分割成多份进行 懒加载异步加载,避免打包成一份后导致体积过大,影响页面的首屏加载;

    • Webpack 中使用 SplitChunksPlugin 进行拆分;

    • 页面 拆分: 不同页面打包成不同的文件;

    • 按功能拆分:

      • 将类似于播放器,计算库等大模块进行拆分后再懒加载引入;
      • 提取复用的业务代码,减少冗余代码;
    • 文件修改频率 拆分: 将第三方库等不常修改的代码单独打包,而且不改变其文件 hash 值,能最大化运用浏览器的缓存;

  • scope hoisting: 作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;

编译性能优化:

  • 升级至 最新 版本的 webpack,能有效提升编译性能;

  • 使用dev-server / 模块热替换 (HMR) 提升开发体验;

    • 监听文件变动 忽略 node_modules 目录能有效提高监听时的编译效率;
  • 缩小编译范围:

    • modules: 指定模块路径,减少递归搜索;
    • mainFields: 指定入口文件描述字段,减少搜索;
    • noParse: 避免对非模块化文件的加载;
    • includes/exclude: 指定搜索范围/排除不必要的搜索范围;
    • alias: 缓存目录,避免重复寻址;
  • babel-loader:

    • 忽略node_moudles,避免编译第三方库中已经被编译过的代码;
    • 使用cacheDirectory,可以缓存编译结果,避免多次重复编译;
  • 多进程并发:

    • webpack-parallel-uglify-plugin: 可多进程并发压缩 js 文件,提高压缩速度;
    • HappyPack: 多进程并发文件的 Loader 解析;
  • 第三方库模块缓存:

    • DLLPlugin 和 DLLReferencePlugin 可以提前进行打包并缓存,避免每次都重新编译;
  • 使用分析:

    • Webpack Analyse / webpack-bundle-analyzer 对打包后的文件进行分析,寻找可优化的地方;
    • 配置profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的地方;
  • source-map:

    • 开发: cheap-module-eval-source-map
    • 生产: hidden-source-map

webpack3和webpack4的区别

1.mode
webpack增加了一个mode配置,只有两种值development | production。对不同的环境他会启用不同的配置。
​
2.CommonsChunkPlugin
CommonChunksPlugin已经从webpack4中移除。
可使用optimization.splitChunks进行模块划分(提取公用代码)。
但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对entry进行拆分
需要设置optimization.splitChunks.chunks = 'all'。
​
3.webpack4使用MiniCssExtractPlugin取代ExtractTextWebpackPlugin。
​
4.代码分割。
使用动态import,而不是用system.import或者require.ensure
​
5.vue-loader。
使用vue-loader插件为.vue文件中的各部分使用相对应的loader,比如css-loader等
​
6.UglifyJsPlugin
现在也不需要使用这个plugin了,只需要使用optimization.minimize为true就行,production mode下面自动为true
​
optimization.minimizer可以配置你自己的压缩程序

webpack如何实现懒加载

  • import()
  • 结合vue或react异步组件
  • 结合vue-router或者react-router异步加载路由

webpack优化构建速度(可用于生产环境)

  • 优化babel-loader、ignorePlugin、noParse、happyPack、paralleIUglifyPlugin

如何利用webpack来优化前端

使用css-loader减小css体积。清除没必要的插件,减小打包体积。利用cdn加速,使用优化图片image-webpack-loader优化图片,提供公共代码。

webpack优化构建速度(不用于生产环境)

  • 自动刷新、热更新、DIIPlugin

通过external配置来提取常用库。利用DIIPlugin和DIIReferencePlugin预编译资源模块,通过DIIPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DIIReferencePlugin将预编译的模块加载进来。

使用happypack实现多线程加速编译。要注意的是,它对file-loader和url-loader支持不好,所以这两个loader就不需要换成happypackk了,其他loader可以类似的转换一下。

使用tree-shaking和scope hoisting来剔除多与代码,使用fast-sass-loader代替sass-loader,babel-loader开启缓存。Babel-loader在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积大冗余,同时也会减慢编译效率,可以加上cacheDirectory参数或使用transform-runtime插件试试。

不需要打包编译的插件库换成全局“script”标签引入的方式,如jQuery插件,react,react-dom等,代码量是很多的,打包起来可能会很耗时,可以直接用标签引用,然后在webpack配置里使用expose-loader或externals或providePlugin提供给模块内部使用相应的变量。

优化构建时的搜索路径。在webpack打包时,会有各种各样的路径要去查询搜索,我们可以加上一些配置,让它搜索地更快。比如说,方便改成绝对路径的模块路径就改一下,以纯模块名来引入的可以加上一些目录路径,还可以善于用下resolve、alias别名,这个字段来配置,还有exclude等的配置。避免多余查找的文件,比如使用babel别忘了剔除不要遍历的。

模块热更新

模块热更新是webpack的一个功能,它可以使代码修改过后不用刷新就可以更新,是高级版的自动刷新浏览器。devServer中通过hot属性可以控制模块的热替换。

Webpack的优点

良好的开发体验,它专注于处理模块化的项目,能做到开箱即用,一步到位。可通过plugin扩展,完整好用又不失灵活。使用场景不局限于web开发,社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展。

Webpack的缺点是只能用于采用模块化开发的项目。