webpack面试题

286 阅读27分钟

1、 前端为何进行打包和构建?

前端代码最终是呈现在浏览器中供用户使用,所以需要兼顾浏览器,用户体验等方面,需要在前端代码方面处理;在开发过程中,前端打包构建也需要一定的流程,统一的打包和构建可提高部门效率

1、前端代码方面:

  • 兼容浏览器:less,postcss,css-loader;vue-loader;
  • 编译高级语言或语法:babel-loader,@babel/preset-env,@babel/preset-typescript;polyfill,core-js,regenerator-runtime;
  • 减少代码体积,压缩代码:terserPlugin压缩js;minicssExtractPlugin css抽离,CssMinimizerPlugin压缩css;压缩代码compressionPlugin;
  • 优化代码,使加载更快:thread-loader多线程;splitChunks将代码拆分;treeShaking,usedExports标记,sifeEffects副作用,PurgeCSSPlugin摇掉css;代码懒加载import(),预加载preLoad,预获取prefetch;

2、研发流程方面:

  • 有统一,高效的开发环境
  • 统一的构建流程和产出标准
  • 集成公司构建规范,(提测,上线等)

2、module,chunk,bundle区别?

  • module是开发中的单个模块,各个源码文件,webpack一切皆模块
  • chunk是代码分割出来的代码块,多模块合并成的,比如entry,output中的多模块,splitChunk
  • bundle是输出的打包文件

3、loader,plugin区别?

  1. loader:
  • loader是转换器,对特定类型进行转换,如将less等转为css
  • loader本质是一个函数,对接收到的内容进行转换,返回转换后的结果,因为webpack只认识javascript,所以对其他类型进行转换的预处理工作
  • loader在module.rules中配置,是一个数组;数组中的每一项都是一个对象,包含test,use(loader,options)等属性
  1. plugin:
  • plugin是扩展插件,对webpack现有内容的扩展,是一个扩展器,基于事件流框架 Tapable
  • 在webpack生命周期中会广播出许多事件,plugin可以在合适的时机,通过API来提供相应的输出
  • plugin在plugins中配置,是一个数组;数组中的每一项都是一个实例,参数通过构造函数传入

4、你常用的loader,plugin有哪些?

1. 常用的loader:

1. 关于css的loader

  • 多线程执行less-loader,thread-loader
  • less转css,less-loader
  • 添加浏览器前缀,postcss-loader
  • css-loader,importLoaders执行前几个loader
  • 将css呈现在页面style-loader
module: {
  rules: [
    // 处理css
    {
      test: /\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1,
            esModule: false
          }
        },
        'postcss-loader'
      ],
      sideEffects: true,
    },
    // 处理less
    {
      test: /\.less$/,
      // webpack使用多个loader是从右到左,从下到上
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 2,
            esModule: false
          }
        },
        'postcss-loader',
        {
          loader: 'thread-loader',
          options: {
            // 一个 worker 进程中并行执行工作的数量
            // 默认为 20
            workerParallelJobs: 2
          }
        },
        'less-loader'
      ]
    },
  ]
},

postcss.config.js

module.exports = {
    plugins: ['postcss-preset-env']
}

2. 关于图片的loader

  • url-loader,处理图片以base64的方式加载到文件内容中,limit大于设置的大小将使用file-loader
module: {
  rules: [
    {
      test: /\.(png|jpg|gif|svg)$/,
      use: [
        {
          loader: 'url-loader',
          options: {
            name: 'img/[name].[hash:6].[ext]',
            limit: 301 * 1024
          }
        }
      ]
    },
  ]
},
  • file-loader,复制图片到打包文件
module: {
  rules: [
    {
      test: /\.(png|jpg|gif|svg)$/,
      use: [
        {
          loader: 'file-loader',
          options: {
            name: 'img/[name].[hash:6].[ext]', 
          }
        }
      ]
    },
  ]
},
  • asset/resource --> file-loader (输出路径);asset/inline --> url-loader (所有都是base64);asset (使用两种url-loader,file-loader)
module: {
  rules: [
    {
      test: /\.(png|jpg|gif|svg)$/,
      type: 'asset',
      generator: {
        filename: 'img/[name].[hash:6][ext]'
      },
      parser: {
        dataUrlCondition: {
          maxSize: 301 * 1024
        }
      }
    },
  ]
},
module: {
  rules: [
    {
      test: /\.(png|jpg|gif|svg)$/,
      type: 'asset/resource',
      generator: {
        filename: 'img/[name].[hash:6][ext]'
      },
    },
  ]
},
module: {
  rules: [
    {
      test: /\.(png|jpg|gif|svg)$/,
      type: 'asset/inline',
    },
  ]
},

3. 处理图标字体

module: {
  rules: [
    {
      test: /\.(ttf|woff2?)$/,
      use: [
        {
          loader: 'url-loader',
          options: {
            name: 'font/[name].[hash:6].[ext]',
            limit: 301 * 1024
          }
        }
      ]
    },
  ]
},
module: {
  rules: [
    {
      test: /\.(ttf|woff2?)$/,
      type: 'asset/resource',
      generator: {
        filename: 'font/[name].[hash:6][ext]'
      }
    },
  ]
},

4. babel处理js,ts

  • babel一般单独设置babel.config.js配置内容
  • @babel/preset-env插件集合,处理es6语法,比如箭头函数
  • core-js,renegerator-runtime是polyfill中两个,用来处理@babel/preset-env处理不了的语法,比如promise,及symbol,async,await等 bable.config.js:
const presets = [
  [
    '@babel/preset-env',
    {
      // false: 默认是false,不对当前的js处理做 polyfill 的填充
      // usage: 依据用户源代码当中所使用的新语法进行填充
      // entry: 依据当前筛选出来的浏览器决定填充什么
      useBuiltIns: 'usage',
      // 默认corejs是2,改为3
      corejs: 3
    }
  ],
  ['@babel/preset-typescript']
  
]
module.exports = {
  presets,
}

webpack.base.js:

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['babel-loader'],
      // 去除node_modules,不做处理
      exclude: /node_modules/,
    },
    {
      test: /\.ts$/,
      use: ['babel-loader']
    }
  ]
},

5. 处理vue转换,vue-loader

  • vue-loader16是对vue3进行打包编译,vue-loader14,15对vue2;vue-loader14只需要配置rules就可以,vue-loader15需要进行插件配置
  • vue热更新也是通过vue-loader
module: {
  rules: [
    {
      test: /\.vue$/,
      use: ['vue-loader']
    }
  ]
},

vue-loader15

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module: {
  rules: [
    {
      test: /\.vue$/,
      use: ['vue-loader']
    }
  ]
},
plugins: [
  new VueLoaderPlugin()
]

2. 常用的plugin:

1. HtmlWebpackPlugin()-webpack.base.js

  • 处理静态文件,生成html文件,也可以使用已经定义好的
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
  new HtmlWebpackPlugin({
    // 地址:需要处理的index.html
    template: 'index.html',
    title: 'index'
  })
]

2. CleanWebpackPlugin()-webpack.prod.js

  • 将上次打包的文件删除,生成这次构建的文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
  new CleanWebpackPlugin()
]

3. DefinePlugin()-webpack.dev.js-webpack.prod.js

  • 定义全局变量 webpack.dev.js
const { DefinePlugin } = require('webpack')
plugins: [
  new DefinePlugin({
      'process.env': config.dev.env
  })
]

webpack.prod.js

const { DefinePlugin } = require('webpack')
plugins: [
  new DefinePlugin({
      'process.env': env
  })
]

4. CopyWebpackPlugin()-webpack.prod.js

  • 有的资源不需要打包,直接拷贝到静态目录
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
  new CopyWebpackPlugin({
    patterns: [
      {
        // 从哪复制的地址
        // to一般简写,直接找output
        from: 'public',
        globOptions: {
          // 忽略的文件,需要添加**表示从from地址下查找资源
          ignore: ['**/index.html']
        }
      }
    ]
  })
]

5. VueLoaderPlugin()-webpack.base.js

  • vue-loader15版本时需要安装插件,vue热更新使用vue-loader+插件
const VueLoaderPlugin = require('vue-loader/lib/plugin')
plugins: [
  new VueLoaderPlugin()
]

6. 关于css,mini-css-extract-plugin,webpack.prod.js

  • mini-css-extract-plugin抽离css
  • 与MiniCssExtractPlugin.loader一起在生产环境使用,开发环境不需要minicssExtractPlugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
plugins: [
  new MiniCssExtractPlugin({
    filename: 'css/[name].[hash:4].css'
  }),
]

webpack.base.js中判断出是生产还是开发环境,生产环境使用MiniCssExtractPlugin.loader,开发环境使用style-loader

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

// base配置信息
const baseConfg = (isProduction) => {
  return {
    module: {
      rules: [
        // 处理css
        {
          test: /\.css$/,
          use: [
            isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1,
                esModule: false
              }
            },
            'postcss-loader'
          ]
        },
      ]
    },
    ......
  }
} 

7. 关于css,css-minimizer-webpack-plugin,webpack.prod.js

  • 压缩css,
  • 生产环境进行压缩,mode:production时包含了css-minimizer-webpack-plugin,不用单独写;开发环境不需要压缩
  • 与minimizer配合使用,只要有压缩的就需要minimizer为true
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
optimization: {
   minimize: true,
   minimizer: [
     new CssMinimizerPlugin()
  ]
},

8. 关于css,purgecss-webpack-plugin,webpack.prod.js

  • css的treeshaking,对于有的css没有被使用,但是打包的时候都会打包,这时可以将未使用的代码treeshaking,不进行打包
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const glob = require('glob')
plugins: [
  new PurgeCSSPlugin({
    // 通过glob插件找到对应目录,哪个目录下的css文件,src目录下所有的css
    paths: glob.sync(`${resolveApp('./src/css')}/**/*`,  { nodir: true }),
    // 期望不被摇掉,默认存在,不会被treeShaking
    safelist: function() {
      return {
        standard: ['body']
      }
    }
  })
]

9. 关于js,terser-webpack-plugin,webpack.prod.js

  • terser-webpack-plugin压缩js代码
  • terser-webpack-plugin依据的是terser工具,主要是解析识别js,去除空格,将长变量改为o等短变量。
  • production默认压缩js,不用配置
  • terset-webpack-plugin与minimizer配合使用,只要有压缩的就需要minimizer为true
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
   minimize: true,
   minimizer: [
     new TerserPlugin()
  ]
},
  • 以前使用uglify压缩js,但是压缩不了es6,现在使用terserplugin

10. ModuleConcatenationPlugin()-webpack.prod.js

  • ModuleConcatenationPlugin作用域提升scope hoisting
  • production已包含此插件,不需要配置
const webpack = require('webpack')
plugins: [
  new webpack.optimize.ModuleConcatenationPlugin()
]

11. inline-chunk-html-plugin,webpack.prod.js

  • 将代码注入到index.html中,代码少的话,不需要再次发送请求
  • 与htmlwebpackplugin一起使用
const InlineChunkHtmlPlugin = require('inline-chunk-html-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
  new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/])
]

12. compression-webpack-plugin,webpack.prod.js

  • 压缩资源,打包到服务器上部署到是压缩后到代码
plugins: [
  new compressionPlugin({
    // 将css,js压缩,出来结果有未压缩的,说明压缩与未压缩相差不大,不需要压缩
    test: /\.(css|js)$/,
    // 压缩比例,压缩后的代码除以压缩前的代码大小
    minRatio: 0.8,
    // 体积大小值才会进行压缩
    threshold: 10240,
    // 压缩算法
    algorithm: 'gzip',
  })
]

13. speed-measure-webpack-plugin,webpack.base.js

  • 查看打包时间,通过打包时间可看到那块使用时间长,可对应进行优化,红色的代表时间长
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
......
module.exports = (env) => {
  const isProduction = env.production
  // 为了在babel.config.js中判断使用
  process.env.NODE_ENV = isProduction ? 'production' : 'development'

  // 根据当前的打包模式来合并配置
  const config = isProduction ? prodConfig : devConfig
  const mergeConfig = merge(baseConfg(isProduction), config)
  return smp.wrap(mergeConfig)
}

14. webpack-bundle-analyzer,webpack.prod.js

  • 对打包内容的分析,可查看大小等
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin plugins: [ 
    new BundleAnalyzerPlugin() 
] 

15. dll库:DllPlugin,DllReferencePlugin,add-asset-html-webpack-plugin

  • 有的共享资源可以抽成dll库,不需要每次都打包,将来使用的时候直接引入库;比如在vue中,每次都需要打包vue,如果把vue抽离成库,将来使用的时候直接导入库就行
  • webpack5后,不使用dll打包速度也很快
  1. 生成dll库,新建dll.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    vue: "vue"
  },
  output: {
    path: path.resolve(__dirname, './dll'),
    filename: 'dll_[name].js',
    // 暴露从入口vue导出的内容,通过script引用
    library: 'dll_[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: 'dll_[name]',
      path: path.resolve(__dirname,'./dll/[name].manifest.json')
    })
  ]
}
  • 打包出来是,dll_vue.js文件,vue.manifest.json文件
  1. 使用dll库-webpack.base.js
  • DllReferencePlugin根据manifest.json找到js
  • 通过add-asset-html-webpack-plugin把dll文件添加到打包目录dist的html中,达到全局变量的效果
plugins: [
  new webpack.DllReferencePlugin({
    // 根据manifest上下文查找js
    context: resolveApp('./'),
    manifest: resolveApp('./dll/vue.manifest.json')
  }),
  new AddAssetHtmlPlugin({
    // 默认地址是auto/...,所以加上
    outputPath: 'auto',
    filepath: resolveApp('./dll/dll_vue.js')
  })
]
// 打包出来index.html
<script defer="defer" src="auto/dll_vue.js"></script>

16. IgnorePlugin()-webpack.prod.js

  • 避开打包一些资源,例如引入moment,但是只使用其中的中文,英文语言,其他的不使用,这个时候可以避免打包moment
plugins: [
  // 忽略moment下的/locale目录
  new webpack.IgnorePlugin({
    resourceRegExp: /\.\/locale/,
    contextRegExp: /moment/
  })
]

5、babel与webpack的区别?

  • babel是新语法编译工具,不关心模块化
  • webpack是打包构建工具,是多个loader,plugin的集合

6、如何产出一个第三方lib?

使用output产出library暴露出

  • libraryTarget,不同平台下的模块化规范
  • library,暴露的名字
  • globalObject,用于改变this指向
const path = require('path')

module.exports = {
  entry: {
    vue: "vue"
  },
  output: {
    path: path.resolve(__dirname, './dll'),
    filename: 'dll_[name].js',
    // 暴露从入口vue导出的内容,通过script引用
    library: 'dll_[name]',
    libraryTarget: 'umd',
    globalObject: 'this'
  }
}

7、babel-polyfill和babel-runtime的区别?

  • babel-polyfill会污染全局
  • babel-runtime不会污染全局,产出第三方lib需要使用babel-runtime

8、webpack如何实现懒加载?为何要进行懒加载?

有的页面一次性加载时,加载比较慢,这时候可以进行按需加载,也就是懒加载,通过完成某些操作后再加载另外一些代码。这样加快了页面的初始加载速度,减轻了总体体积,是一种很好的网页优化方式。

懒加载一般伴随着代码分离,代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级。

代码分离方式:

1. 多入口。

entry: {
  // 代码拆分,第一种:多入口 ,不常用
  login: './src/js/login.js',
  hmr: './src/js/hmr.js',
},
output: {
  filename: 'js/[name].build.js',
  path: path.resolve(__dirname, 'dist')
},

打包出来是两个文件,login.build.js;hmr.build.js

2. 多入口,添加依赖防止重复。

entry: {
  // 第二种,防止重复,使用dependOn在多个chunk之间共享模块;添加依赖,lodash,jquery就是依赖,打包出来的shared包括lodash,jquery
  login: {import:'./src/js/login.js', dependOn: 'shared'},
  hmr: {import:'./src/js/hmr.js', dependOn: 'shared'},
  shared: ['lodash', 'jquery']
},
output: {
  filename: 'js/[name].build.js',
  path: path.resolve(__dirname, 'dist')
},

打包出来是四个文件,login.build.js;hmr.build.js;shared.build.js(包含lodash,jquery);还有shared.txt

  • 去除依赖的txt文件
const TerserPlugin = require('terser-webpack-plugin')
optimization: {
  minimizer: [
    new TerserPlugin({
      // 第二种拆分,拆分打包后会有.txt文件,去除txt文件
      extractComments: false,
    })
  ],
},

3. 单文件入口,通过splitchunks分离代码

splitchunks可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk

entry: {
  // 第三种,单文件入口,通过splitchunks拆分公共的依赖模块
  index: './src/index.js'
},
output: {
  filename: 'js/[name].build.js',
  path: path.resolve(__dirname, 'dist')
},
optimization: {
  splitChunks: {
    chunks: 'all'
  }
},

打包出来一个文件,index.build.js;

  • chunks: 'all'同步异步都进行拆分

4. 懒加载:触发相应条件进行动态导入

  • 例如在lazyload.js中加载lazy1.js文件
const oBtn = document.createElement('button')
oBtn.innerHTML = '点击加载元素'
document.body.appendChild(oBtn)

// 按需加载
oBtn.addEventListener('click', () => {
  import('./lazy1').then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})
  • webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,所以需要获取default中的值。
  • 进入浏览器显示lazyload.js的打包文件,不显示lazy1.js的打包文件,等点击加载元素后才会显示 缺点: 发送请求,如果文件很大,用户能够感知到页面加载

5. preLoad预加载,prefetch预获取

  1. preload与prefetch区别:
  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  1. 用法:
  • webpackChunkName:打包后的名字
  • webpackPrefetch
const oBtn = document.createElement('button')
oBtn.innerHTML = '点击加载元素'
document.body.appendChild(oBtn)

// 按需加载
oBtn.addEventListener('click', () => {
  import(
    /*webpackChunkName: 'lazy12' */
    /*webpackPrefetch: true */
    './lazy1'
    ).then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})

index加载出来,可能lazy12也一起加载出来,但是加载顺序是index.js,然后是lazy12.js

  • webpackPreLoad
const oBtn = document.createElement('button')
oBtn.innerHTML = '点击加载元素'
document.body.appendChild(oBtn)

// 按需加载
oBtn.addEventListener('click', () => {
  import(
    /*webpackChunkName: 'lazy12' */
    /*webpackPreLoad: true */
    './lazy1'
    ).then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})

index加载出来,点击加载元素按钮,lazy12加载出来

  • 在项目中,比如有多个tab,A,B,C; B,C可以使用webpackPrefetch空闲时加载;首页A页面,使用preLoad并行加载

9、为什么proxy不能被polyfill?

vue3中使用proxy进行的响应式监听

如Class能被function模拟;promise能被callback模拟;但proxy的作用不能用Object.defineProperty来模拟

10、热更新是什么?

没有使用热更新之前,每次修改完代码都需要在index.html中通过右键open with Live Server打开代码在浏览器中。

想要修改完代码直接在浏览器中自动刷新,有以下方式:

1. watch与live server一起使用,也是文件监听

  • 在package.json中添加--watch,侦听
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack --watch"
},
  • 在webpack.config.js中添加watch:true
module.exports = {
  watch: true,
  ......
}

这两种不足的地方:

  • 所有源代码都会重新编译
  • 每次编译成功之后都需要进行文件读写,CleanWebpackPlugin
  • 使用vscode插件的live server
  • 不能实现局部刷新 文件监听原理:
  • 轮询判断文件的最后编辑时间是否变化,如果某个文件变化了,不会马上告诉监听者,而是先缓存起来,等aggregateTimeout 后再执行。
module.exports = {
  // 默认false,也就是不开启
  watch: true,
  watchOptions: {
    // 默认为空,不监听的文件或者文件夹,支持正则匹配
    ignored: /node_modules/,
    // 监听到变化发生后会等300ms再去执行,默认300ms
    aggregateTimeout:300,
    // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
    poll:1000
  }
  ......
}

2. webpack-dev-server

  • 安装npm install webpack-dev-server -D
  • package.json配置serve
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack",
  "serve": "webpack serve"
},

如果build配置了--config名,serve也需要

"build": "webpack --config lg.webpack.js",
"serve": "webpack serve --config lg.webpack.js"
  • 终端npm run serve打开localhost:8080/,更新后在页面中自动更新

优点:

  • 不需要使用live server,直接使用webpack的webpack-dev-server就可以,与webpack统一
  • webpack-dev-server可以实现热更新
  • 不需要再重新读写,直接在内存(node_modules/webpack-dev-server)中读取就行,服务器在内存中读取内容,远比在硬盘中读取速度要快

3. webpack-dev-middleware定制打包过程

一般用的比较少

4. 热更新(Hot Module Replacement HMR)

热更新是想要刷新某一模块,不需要全部刷新

  • 开发模式下将.browserslistrc屏蔽
  • 设置具备热更新功能,hot: 'only'
module.exports = {
  mode: 'development',
  // 开发模式下将.browserslistrc屏蔽
  target: 'web',
  // 具备热更新功能
  devServer: {
    // 功能开始,默认情况hot: true还是刷新整个页面
    // only:多个模块一起显示时,当一个模块有错误,只改此模块,不影响别的
    hot: 'only',
    // devServer的其他属性:
    // 打开的端口号
    port: 8001,
    // 是否自动打开
    open: false,
    // 压缩,性能提升
    compress: true,
    // 将404页面用index.html代替
    historyApiFallback: true,
    // 代理
    proxy: {
      '/api': {
        // 例如接口名: /api/users,将http://localhost:8001/api/users代理到https://api.github.com/users
        // 代理地址
        target: 'https://api.github.com',
        // 将/api改为接口中需要的地址
        pathRewrite: { "^/api": '' },
        // changeOrigin默认是false:请求头中host仍然是浏览器发送过来的host
        // 设为true:发送请求头中host会设置为target中的
        changeOrigin: true
      }
    }
  },
  ......
}
  • index.js将需要热更新的文件引入,并配置
// index.js
import './js/hmr'

if(module.hot) {
  module.hot.accept(['./js/hmr.js'],() => {
    // 后续业务处理,有需要加
    console.log('hmr模块更新')
  })
}
  • npm run serve,修改hmr页面,会进行热更新,不刷新页面,只修改局部

11、说一下webpack热更新的原理?

  • HMR的核心是,客户端从服务器端拉取更新后的文件,准确的说是chunk diff(chunk 需要更新的部分),
  • 实际上webpack-dev-server与客户端之间维护了一个webSocket,当本地资源发生改变时,webpack-dev-server通知客户端有更新内容,并将hash值发送给客户端,让客户端与上一次资源进行对比;
  • 客户端对比后,发送ajax请求,获取更新内容;
  • webpack-dev-server发送更新内容给客户端;
  • 客户端根据更新内容发送jsonp请求获取该chunk的增量更新
  • 后续由hotMoudlePlugin来完成,根据各自的场景使用相关API,比如vue-loader进行热更新

12、webpack常见的性能优化方式?

1. 优化构建速度

  • 自动刷新(webpack-dev-server);
  • 热更新(HMR);
  • DllPlugin(),将公有依赖抽成库,打包成js,manifest.json;通过DllReferencePlugin()和add-asset-html-webpack-plugin添加到index.html中,不需要每次都打包
  • thread-loader多线程,将loader同步执行转为并行,以前使用的happyPack()

2. 优化产出代码

  • 小图片使用base64编码,url-loader使用Limit

  • bundle加hash

  • 压缩代码:压缩css,css-minimizer-webpack-plugin;压缩js,terserPlugin;

  • tree shaking摇掉不需要的代码:摇掉css,purgecss-webpack-plugin;摇掉js,usedExports标记不被使用的,配合terserPlugin一起使用;sideEffects,去除副作用

  • scope hoisting作用域提升,moduleConcatenationPlugin()

  • IgnorePlugin()避免打包一些资源

  • 懒加载 import()

  • 提取公共代码:splitchunks可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk

  • 使用CDN加速:第三方包抽离出来,不进行部署服务器,剩下的打包到服务器,比如对lodash不进行打包,通过cdn的url设置引入lodash到index.html

externals: [{
    lodash: '_'
}],
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  • 使用production

13、webpack3和webpack4的区别?

  • webpack4新增了mode,mode有development开发模式侧重于构建速度,production生产模式侧重于打包后的文件大小
  • webpack3移除了loaders,webpack3是loaders与rules共存;webpack4只有rules
  • webpack3提取公共代码CommonsChunkPlugin;webpack4中使用optimizaration.splitChunks提取公共代码
  • webpack4支持es6的方式导入json,并且可以过滤代码

14、webpack与gulp的不同?还有那些打包工具?

webpack是模块打包工具,它可以递归打包各个模块,最终生成几个打包后的文件

webpack与其他的工具最大的不同在于他支持code-splitting(把代码分成很多块chunk,主要有分离代码,懒加载)、模块化(AMD,ESM,CommonJs)、全局分析。

  • gulp处理一些轻量化任务,例如单独打包css文件
  • gulp是基于任务和流,找到一个文件,对其进行链式操作,更新流上的数据,整条链式操作构成一个任务,多个任务构成web构建流程
  • webpack是基于入口的,webpack会自动解析递归解析入口所需要加载的所有资源文件,然后用不同的loader处理不同的文件,用plugin来扩展webpack功能
  • gulp更像后端开发者思想,需要对整个过程了解,webpack更像前端开发者的思路

还有rollup,parcel等打包工具 从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果, 由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

15、webpack的构建过程?

  • 初始化参数:从配置文件或shell语句中读到参数的合并,得到最终的参数
  • 开始编译:用最终的参数初始化complier对象,加载所有配置的插件,执行run方法开始编译
  • 确定入口:通过entry确定入口
  • 编译模块:从入口文件出发,调用所有的loader对模块进行解析编译,找到该模块依赖的模块进行编译
  • 完成模块编译:得到每个模块被翻译后的最终内容和依赖关系
  • 输出资源:根据入口和每个模块的依赖关系,组装成一个个包含多个模块的chunk,在把每个chunk转换成一个单独的文件加载到输出列表
  • 输出完成:确定输出的路径和文件名,把内容写到文件系统中

16、如何自动生成webpack配置?

通过webpack-cli,vue-cli等脚手架工具

17、webpack-dev-serve和http服务器如nginx有什么区别?

  • webpack-dev-serve用内存来存储打包文件
  • webpack-dev-serve可以使用热更新
  • webpack-dev-serve比传统的http服务器来说,对开发更简单高效

18、什么是长缓存?在webpack中怎么做到长缓存优化?

浏览器在用户访问的时候,为了加快加速度,会对静态资源进行缓存,但是代码每次更改后,浏览器都需要重新加载,更方便和简单的方式就是引入新的文件名称。

  • webpack中可以在output输出的文件指定chunkhash,并且分离经常更新的代码和框架代码。
  • 通过NameModulesPlugin或是HashedModuleIdsPlugin使再次打包文件名不变。

19、tree shaking的原理?

tree shaking是将不用的但是被写或者引入的代码摇掉。

1. tree shaking有三种方式:

  1. usedExports标记不被使用的,配合terserPlugin一起使用 例如untils.js文件有sum,square两个函数,在index.js文件中只对sum使用
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'development',
  devtool: false,
  optimization: {
    usedExports: true, // 标记不被使用是函数
    minimize: true,
    minimizer: [
      new TerserPlugin()
    ]
  },
}

打包后,只能搜索到sum函数

  1. sideEffects,去除副作用 例如,在util.js中,需要去除util.js的副作用,不需要去除login.js副作用
  • index.js引入时,不能用任何值来接受导出的值
// 副作用,sideEffects
import './js/until'
import './js/login'
  • package.json配置sideEffects
// 数组中写不需要去除副作用的文件地址
"sideEffects": [
  "./src/js/login.js"
],
  • css如果不需要去除副作用一般 在rules中配置sideEffects:true,所有的都不用去除副作用
rules: [
  // 处理css
  {
    test: /\.css$/,
    use: [
      'style-loader',
      {
        loader: 'css-loader',
        options: {
          importLoaders: 1,
          esModule: false
        }
      },
      'postcss-loader'
    ],
    sideEffects: true,
  },
  1. PurgeCSSPlugin()插件,对于有的css没有被使用,但是打包的时候都会打包,这时可以将未使用的代码treeshaking,不进行打包
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const glob = require('glob')
plugins: [
  new PurgeCSSPlugin({
    // 通过glob插件找到对应目录,哪个目录下的css文件,src目录下所有的css
    paths: glob.sync(`${resolveApp('./src/css')}/**/*`,  { nodir: true }),
    // 期望不被摇掉,默认存在,不会被treeShaking
    safelist: function() {
      return {
        standard: ['body']
      }
    }
  })
]

2. 原理

一是先标记出模块导出值中哪些没有被动用过,二是 Terser 使用删除掉这些没被用到的导出语句。

标记过程大致可划分为三个步骤:

  • Make 阶段:收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中。
  • Seal 阶段:遍历 ModuleGraph 标记模块导出变量有没有被使用
  • 生成产物时:若变量没有被其他模块使用则删除对应的导出语句。

Webpack 中 Tree Shaking 的实现分为如下步骤:

  • FlagDependencyExportsPlugin 插件中根据模块的 dependencies 列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo
  • FlagDependencyUsagePlugin 插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime 集合中
  • HarmonyExportXXXDependency.Template.apply 方法中根据导出值的使用情况生成不同的导出语句
  • 使用 DCE 工具删除 Dead Code,实现完整的树摇效果

20、babel的原理?

大多数javascript parse解析遵循estree规则,Babel最初基于acorn项目(轻量级javascript解析器)

babel大概分为三部分:

  • 解析:将代码解析为AST抽象语法树,每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon实现的。在解析过程中有两个阶段:词法分析语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。
  • 转换:babel接收到的AST,通过babel-traverse进行深度优先遍历,对节点进行添加,更新,移除操作
  • 生成:将转换过的AST通过babel-generator进行深度优先遍历再转换成js代码

21、有写过loader吗,简单阐述下思路?

因为loader支持链式调用,所以开发上需要严格遵循‘单一职责’,每个loader只负责自己的事情。

  • loader 拿到源文件的内容(content),通过this.getOptions() 拿到传入的参数
  • 通过返回值的方式将处理后的内容输出或者通过 this.callback() 同步方式将内容返回出去,也可以调用 this.async() 生成一个异步的函数
  • callback 处理传入的内容,再通过调用 cabllback()将处理后的内容返回出去。

开发的过程中尽量使用异步 loader。使用 schema-utils 来检验参数,然后再利用第三方提供的模块进行 loader 的开发。

22、有写过plugin吗,简单阐述下思路?

webpack 在运行生命周期中会广播出许多事件,PLugin 可以监听这些事件,在特定的阶段写入想要添加的自定义功能。

webpack 的 tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

  • 通过 consturctor 获取传入的配置参数
  • apply() 方法得到 compiler
  • compiler 暴露了和 webpack 整个生命周期相关的钩子
  • 通过 conpiler.hooks.thiscompilation 初始 compilation,* compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
  • 再使用相关的 hooks 对资源进行添加或者修改
  • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 webpack 输出资源的最后时机)。

异步的事件需要再插件处理完任务时调用回调函数通知 webpack 进入下一个流程,不然会卡住。

23、source-map是什么?生产环境怎么使用?

source-map是代码地图devtool的一个属性,能够查看源代码,因为打包后的代码是经过压缩处理的代码,所以如果出现问题,查看不到写道源代码,source-map是可以看到源代码的属性值

devtool的几个常用开发环境属性:

  • source-mapnpm run build会产生.map文件;在build.js中最后引入//# sourceMappingURL=build.js.map;浏览器报错信息定位到源代码文件中;vue组件使用的一般是source-map
  • cheap-module-source-mapnpm run build会产生.map文件;在build.js中最后引入//# sourceMappingURL=build.js.map,中间也穿插base64格式的sourceMappingURL;浏览器报错信息定位到源代码文件中;react组件使用的一般是cheap-module-source-map
  • eval-source-mapnpm run build不会产生.map文件;在build.js中间穿插base64格式的sourceMappingURL;浏览器报错信息定位到打包文件;
  • inline-source-mapnpm run build不会产生.map文件;在build.js中间,结尾穿插base64格式的sourceMappingURL;浏览器报错信息定位到源代码文件中;

线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台Sentry 使用
  • nosources-source-map:只会显示错误的行数以及查看源代码的错误栈,安全系数比source-map高
  • source-map:通过nginx 设置将 .map 文件只对白名单开放(公司内网) 注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

24、文件指纹是什么?

文件指纹是指打包输出的文件后缀。

  • hash:和整个项目的构建有关,只要项目文件有更改,构建的hash就会有变化
  • chunkhash:和webpack打包的chunk有关,不同的entry,不同的chunk,会生成不同的chunkhash
  • contenthash:根据文件内容来定义hash,文件内容不发生改变,contenthash就不会变化

25、使用webpack时,使用过那些提高效率的插件?

  • webpack.merge:提取公共配置,减少重复配置
  • speed-measure-webpack-plugin:查看打包时间,可对时间长的loader,plugin进行优化
  • webpack-bundle-analyzer:查看打包内容,大小等

26、模块打包原理知道吗?

webpack实际上为模块创造了一个可以导出和导入的环境,没有改变源代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。

27、如何对bundle体积进行监控和分析?

  • vscode中有Import Cost插件,对引入的模块大小进行实时监测
  • webpack-bundle-analyzer插件,可查看打包后的内容,大小
  • bundlesize工具包可以进行自动化资源体积监控

28、在实际工程中,配置文件上百行是常事,如何保证loader按照预期方式工作?

使用enforce强制执行loader的执行顺序;pre代表在所有正常loader执行前执行;post是所有loader执行后执行。(inline官方不推荐使用)

29、怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可

多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是得遵循他预设好的项目目录结构。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

30、npm打包时需要注意那些?如何利用webpack来更好构建?

NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loaderextract-text-webpack-plugin来实现,配置如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        // 增加对 CSS 文件的支持
        test: /.css/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader']
        }),
      },
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      // 输出的 CSS 文件名称
      filename: 'index.css',
    }),
  ],
};

31、webpack proxy的工作原理?为什么能解决跨域?

1. webpack proxy是什么

webpack proxy,即webpack提供的代理服务,基本行为就是接收客户端发送的请求后转发给其他服务器,其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)

想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server

webpack-dev-server只适用于开发环境

配置:

module.exports = {
  mode: 'development',
  // 开发模式下将.browserslistrc屏蔽
  target: 'web',
  // 具备热更新功能
  devServer: {
    // 功能开始,默认情况hot: true还是刷新整个页面
    // only:多个模块一起显示时,当一个模块有错误,只改此模块,不影响别的
    hot: 'only',
    // devServer的其他属性:
    // 打开的端口号
    port: 8001,
    // 是否自动打开
    open: false,
    // 压缩,性能提升
    compress: true,
    // 将404页面用index.html代替
    historyApiFallback: true,
    // 代理
    proxy: {
      '/api': {
        // 例如接口名: /api/users,将http://localhost:8001/api/users代理到https://api.github.com/users
        // 代理地址
        target: 'https://api.github.com',
        // 将/api改为接口中需要的地址
        pathRewrite: { "^/api": '' },
        // changeOrigin默认是false:请求头中host仍然是浏览器发送过来的host
        // 设为true:发送请求头中host会设置为target中的
        changeOrigin: true
      }
    }
  },
  ......
}

2. 原理

proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器

举个例子:

在开发阶段,本地地址为http://localhost:3000,该浏览器发送一个带有/api标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中

3. 跨域

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

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

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

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

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

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