前端Webpack面试题

备注:本文是总结他人资料,供面试背诵使用

1.说说你对webpack的理解

​ 开发时,我们会使用框架 (React、Vue) ,ES6 模块化语法,Less/Sass 等 CSS 预处理器等语法进行开发,这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、CSS语法才能运行。所以我们需要打包工具帮我们做完这些事。除此之外,打包还能压缩代码做兼容性处理提升代码性能等。
​ webpack 是一个静态模块的打包工具。它会在内部从一个或多个入口点构建一个依赖图,然后将项目中所需的每一个模块组合成一个或多个 bundles 进行输出,它们均为静态资源。输出的文件已经编译好了,可以在浏览器运行。 webpack 具有打包压缩、编译兼容、能力扩展等功能。其最初的目标是实现前端项目的模块化,也就是如何更高效地管理和维护项目中的每一个资源。

2.说说webpack的构建流程

初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译

确定入口:根据配置中的 entry 找出所有的入口文件

编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表

输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到事件后会执行特定的逻辑,并且插件可以调用webpack提供的API改变webpack的运行结果

3.Loader是什么?

​ 其作用是让 Webpack 能够去处理那些非 JavaScript 文件。由于 Webpack 自身只理解 JavaScript、JSON ,其他类型/后缀的文件都需要经过 loader 处理,并将它们转换为有效模块。loader 可以是同步的,也可以是异步的;而且支持链式调用,链中的每个 loader 会处理之前已处理过的资源。
loader的两个属性:
1.test,识别出哪些文件会被转换
2.use,在进行转换时,使用的loader
多loader的执行顺序,从右到左(从下到上)

4.常用的Loader

babel-loader 将ES6代码转换成ES5版本

ts-loader ts转js 如果没有babel,使用ts-loader,有babel用@babel/preset-typescript

sass-loader sass转换css

style-loader 创建

css-loader 将css转为js模块导出

url-loader url-loader可以在图片大小小于设定的limit的时候返回的是base编码的图片,大于limit时会调用file-loader对图片进行处理。

file-loader 其实就是将文件拷贝到输出目录下,使用file-loader中配置的新名字

postcss-loader (1)将css解析成js可以操作的AST,(2)调用插件来处理AST并得到结果。

经常配合autoprefixer自动为CSS添加浏览器前缀

html-loader 将html代码转换为js模块导出

eslint-loader 检查代码是否符合eslint规范,只检查错误,不修改代码

5.手写Loader

const loaderUtils = require('loader-utils');

module.exports = function(source){

  // 知识点一  如果为每个构建执行重复的转换操作,这样webpack的构建可能会变得非常慢
  // 开启缓存
  this.cacheable && this.cacheable();

  // 知识点二  使用loaderUtils 获取loader的参数
  const options = loaderUtils.getOptions(this);
  console.log('options ->', options);

  // 知识点三  告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果 
  // 异步用this.axync() 同步用this.callback()
  var callback = this.async()    
  // someAsyncOperation 代表一些异步的方法
  someAsyncOperation(source).then(res => {
      // 通过 callback 返回异步执行后的结果
      callback(err, result, sourceMaps, ast)
  })



  // 真正的手写代码
  const reg = /(console.log((.*)))/g;
  source = source.replace(reg, '');
  this.callback(null, source);
  // return undefined的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
  return;
}

/** 
this.callback(    
  // 当无法转换原内容时,给 Webpack 返回一个 Error   
  err: Error | null,    
  // 原内容转换后的内容    
  content: string | Buffer,    
  // 用于把转换后的内容得出原内容的 Source Map,方便调试
  sourceMap?: SourceMap,    
  // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能 
  abstractSyntaxTree?: AST
); 
*/

6.Plugins是什么?

​ 由于webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的时机执行自己的插件任务,webpack有两个核心对象:

​ compiler:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子

​ compilation:作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 Compilation 将被创建

1.插件必须是一个带有apply方法的对象

2.不建议修改compiler和compilation对象,因为传入的同一个对象的引用

class CopyrightWebpackPlugin {
  // 编写一个构造器
  // 参数初始化
  constructor(options) {
       console.log(options)
   }

  apply(compiler) {
      //遇到同步时刻   同步用tap
      compiler.hooks.compile.tap('CopyrightWebpackPlugin',() => {
          console.log('compiler');
      });

      //遇到异步时刻  异步用tapAsync
      // emit 输出asset到output目录之前执行
      //Compilation存放打包的所有内容,Compilation.assets放置生成的内容
      compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (Compilation, callback) => {
          debugger;
          // 往代码中增加一个文件,copyright.txt
          Compilation.assets['copyright.txt'] = {
              source: function() {
                  return 'copyright by monday';
              },
              size: function() {
                  return 19;
              }
          };
          callback();
      })
  }
}

module.exports = CopyrightWebpackPlugin;

7.常用的Plugin

1.HtmlWebpackPlugin

2.CleanWebpackPlugin

3.CopyWebpackPlugin

4.Webpack.DefinePlugin 定义全局变量

5.MiniCssExtractPlugin 将css抽取成单独的文件

6.PurgeCSSPlugin 删除没用到的CSS样式,需要判断引用文件,起到css tree-shaking的效果

7.webpack.ProvidePlugin 配置全局模块,避免多次引入的麻烦

8.BundleAnalyzerPlugin 分析打包后资源的依赖以及大小

9.ImageminPlugin 压缩图片

10.CompressionPlugin 启用传输压缩,gzip压缩,需要服务端配合

11.webpack.NoEmitOnErrorsPlugin 遇到编译报错不输出

12.OptimizeCssAssetsPlugin 压缩css,去除重复的类名样式,去除无用的空格

13.UglifyJSPlugin 压缩JS

14.webpack.HotModuleReplacementPlugin 热更新插件

15.TerserPlugin 压缩es6,相比UglifyJsPlugin插件,能更好的处理ES6以上的语法

8.实现一个Plugin

​ 由于webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的时机执行自己的插件任务,webpack有两个核心对象:

​ compiler:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子

​ compilation:作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 Compilation 将被创建

在这里插入代码片class CopyrightWebpackPlugin {
  // 编写一个构造器
  // 参数初始化
  constructor(options) {
       console.log(options)
   }

  apply(compiler) {
      //遇到同步时刻   同步用tap
      compiler.hooks.compile.tap('CopyrightWebpackPlugin',() => {
          console.log('compiler');
      });

      //遇到异步时刻  异步用tapAsync
      // emit 输出asset到output目录之前执行
      //Compilation存放打包的所有内容,Compilation.assets放置生成的内容
      compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (Compilation, callback) => {
          debugger;
          // 往代码中增加一个文件,copyright.txt
          Compilation.assets['copyright.txt'] = {
              source: function() {
                  return 'copyright by monday';
              },
              size: function() {
                  return 19;
              }
          };
          callback();
      })
  }
}

module.exports = CopyrightWebpackPlugin;

9.Source map是什么?

source map是将编译、打包、压缩后的代码映射和回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试代码就需要source-map,提高开发效率

10.文件指纹是什么?

文件指纹是打包文件的唯一标识,文件指纹通常有两个用途:

  • 版本管理: 在发布版本时,通过文件指纹来区分 修改的文件 和 未修改的文件。

  • 使用缓存: 未修改的文件,文件指纹保持不变,浏览器继续使用缓存访问。

    Hash 是和整个项目的构建相关,compilation 实例的变化就会触发 Hash 的变化。

    Chunkhash 是和 webpack 打包的模块相关,每一个 entry 作为一个模块,会产生不同的 Chunkhash 值,所以他们之间的变化是互不影响的。

    Contenthash 是和根据文件内容相关,比如一个页面内的 JS 内容、CSS 内容都会拥有自己的 Contenthash,可以保持各自的独立更新。

11.说说webpack热更新的原理

​ HMR全称Hot Module Replacement,可以理解为模块热替换,指在应用程序运行过程中,替换,添加,删除模块,而无需重新刷新整个应用。
​ 例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失如果使用的是 HMR,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用

img

webpack的原理:

  • 启动阶段
    1-2-A-B 在编写为经webpack打包的源代码后,webpacl compiler将源代码和HMR Runtime一起编译成bundle文件,放在webpack Dev Server,当浏览器请求bundle.js文件时,bundle.js就返回给浏览器,运行在浏览器上
    (socket.js在服务器和浏览器建立了一个websocket长链接)

  • 更新阶段
    1-2-3-4 当某一个文件或者模块发生变化时,webpack监听到文件变化对文件重新编译打包,编译生成唯一的hash值,本次生成的hash值会作为下一次热更新的标识,也就是本次热更新使用的hash值,是上一次热更新生成文件的hash(此次的hash值是上次热更新中manifest中h的值)

    由于socket服务器在HMR RuntimeHMR Server之间建立 websocket链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,如下图的h属性,作为下一次热更细的标识

请添加图片描述

浏览器根据hash标识,去创建ajax去获取此次的mainfest(h:build生成的hash值,c:变化的模块 )文件,使用jsonp获取此次的修改内容,浏览器拿到这两个文件后,通过HMR runtime机制,加载这两个文件,触发render流程,实现局部刷新

12.说说webpack proxy工作原理

​ 在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上

所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题

通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者

当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地

img

在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据

注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制

13.webpack打包体积优化

一、提取公共模块

假如现在有一个MPA(多页面应用)的react项目,每个页面的入口文件及其依赖的组件中都会引入一份react和react-dom等,那最终打包后的每个页面中同样也会有一份以上的公共包的代码,可以将这个包单独抽离出来,最终在每个打包后的页面入口文件引入,从而减少打包后的总体积

module.exports = {
  optimization: {
    splitChunks: {
      minSize: 20000,
      cacheGroups: {
        react: {
          test: /(react|react-dom)/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  }
};

二、配置CDN服务器

1.可以修改output:{publicPath: ‘’} 的值,打包时添加上自己的CDN地址

2.在CDN上部署自己依赖的第三方资源,

3.在externals中加入不进行打包的资源,在html模版中加入CDN服务器地址

在这里插入图片描述

4.如果没有自己配置CDN服务器,可以使用其他人放置在cdn服务器上的资源

三、代码压缩(此处的代码压缩并非真正的压缩,而是删掉代码中无意义的部分,例如空格)

1.压缩js,使用插件TerserPlugin

在这里插入图片描述

2.压缩css,使用CSSMinimizerPlugin

css压缩通常是去除无用的空格等,因为很难去修改选择器,属性和名称、值等

在这里插入图片描述

3.压缩html,使用HtmlWebpackPlugin的minify配置

new HtmlWebpackPlugin({
    template: path.join(__dirname, './public/index.html'),
    filename: 'index.html',
    minify: {            // minify配置可以压缩html文件,设置了minify,实际上会使用另一个插件html-minifier-terser
      minifyCSS: false,                    //是否压缩css
      collapseWhitespace: true,            // 是否折叠空格
      removeComments: true,                // 是否移除注释
    }
}),

在这里插入图片描述

4.压缩图片,使用image-webpack-loader

这个不太理解,压缩后图片会不会失真或者像素变低?打包的时候压缩,使用的时候进行解压缩?

四、使用TreeShaking

tree shaking是一个术语,在计算机中标识消除死代码,尽量使用纯函数编程,用于消除未调用的代码

1.JS实现Tree Shaking

(1)usedExports,配置方法很简单,只需将usedExports设置为true

module.exports = {
    ...
    optimization:{
        usedExports
    }
}

使用后,没被用上的代码在webpack打包中会添加 unused harmony export … 注释,用来告知Terser在优化时,可以删掉这段代码

在这里插入图片描述

(2)sideEffects实现

sideEffects用于指定哪些模块是有副作用的,副作用指的是这里面的代码有执行一些特殊的任务,不能仅仅依靠export来判断这段代码的意义

sideEffect设置为false,就是告知webpack可以安全的删除未用到的exports

sideEffect设置为数组,则将代码中的数据进行保留

在这里插入图片描述

总结:

  • 在optimization中配置usedExports为true,来帮助Terser进行优化;
  • 在package.json中配置sideEffects,直接对模块进行优化;

2.CSS实现Tree Shaking

css主要使用的purgecss-webpack-plugin

在这里插入图片描述

五、文件大小压缩(真正意义的压缩,gzip算法)

compression-webpack-plugin 插件

对文件的大小进行压缩,将前端打包好的资源文件进一步压缩,生成指定的、体积更小的压缩文件,减小http传输过程中带宽的损耗

new ComepressionPlugin({
    test:/\.(css|js)$/,  // 哪些文件需要压缩
    threshold:500, // 设置文件多大开始压缩
    minRatio:0.7, // 至少压缩的比例
    algorithm:"gzip", // 采用的压缩算法
})

​ 使用npm run build,会生成许多以.gz格式的文件
​ 生成压缩后的文件,不能直接使用,需要服务端配置才可以使用,而且发现打包生成的“dist/index.html”首页内,也没有直接引用这些“.gz”格式的文件。
​ 而实现的关键,其实就是让服务端向浏览器发送“Content-Encoding=gzip”这个响应头,并把对应的“.gz”格式文件发送给浏览器,让浏览器通过“gzip”编码格式来解析资源。

const path = require('path');
const fs = require('fs');
const express = require('express');
const app = express();

app.use((request, response, next) => {
 const fullPath = path.join(__dirname, `${request.originalUrl}.gz`);     
 
 // 检测是否存在同名.gz压缩文件
 if (fs.existsSync(fullPath)) {
     // 存在就告诉浏览器用gzip编码格式来解析,并把对应的“.gz”格式文件发送给浏览器。
     response.setHeader('Content-Encoding', 'gzip')  
     response.sendFile(fullPath);
 } else {
     next()
 }
})
app.use(express.static('./'));

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

六、antd按需导入

import {Col,Row} from 'antd'

14.优化构建速度

这个问题个人认为比较宽泛,顺着聊几点就好了,答不上来也没关系,重点是上面的压缩打包后的体积

可以使用speed-measure-webpack-plugin统计总打包耗时以及每个plugin和loader的打包耗时

一、缩小构建范围、优化Loader配置

在使用loader时,可通过配置include、exclude、test属性来匹配文件

module.exports = {
  module: {
    rules: [
      {
        // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
        test: /\.js$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
    ]
  },
};

二、优化resolve.alias

alias给一些常用的路径起一个别名,特别当我们的项目目录结构比较深的时候,一个文件的路径可能是./../../的形式

通过配置alias以减少查找过程

module.exports = {
    ...
    resolve:{
        alias:{
            "@":path.resolve(__dirname,'./src')
        }
    }
}

三、多进程构建

对于耗时较长的模块,同时开启多个 nodejs 进程进行构建,可以有效地提升打包的速度。terser启动多线程

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};

四、合理使用source-map

打包生成sourceMap的时候,信息越详细,打包速度就越慢

五、cache-loader

在一些性能开销较大的 loader之前添加 cache-loader,以将结果缓存到磁盘里,显著提升二次构建速度

保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此loader

module.exports = {
    module: {
        rules: [
            {
                test: /\.ext$/,
                use: ['cache-loader', ...loaders],
                include: path.resolve('src'),
            },
        ],
    },
};
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值