前端面试的那些事儿 ~ 深刻浅出 webpack 之基础应用篇

前言

前端工程化经历过不少优秀的工具,例如 Grunt、Gulp、webpack、rollup 等等,每种工具都有本身适用的场景,而现今应用最为普遍的当属 weback 打包了。所以 webpack 也天然而然成了面试官打探你是否懂前端工程化的重要指标。javascript

因为 webpack 技术栈比较复杂,所以决定分如下几篇文章全面深刻的讲解:css

  • 基础应用篇
  • 高级应用篇
  • 性能优化篇
  • 原理篇( webpack 框架执行流程、手写 plugin、手写 loader )

webpack 是什么

webpack 是模块打包工具html

webpack 能够不进行任何配置(不进行任何配置时,webpack 会使用默认配置)打包以下代码:前端

// moduleA.js
function ModuleA(){
  this.a = "a";
  this.b = "b";
}

export default ModuleA


// index.js
import ModuleA from "./moduleA.js";

const module = new ModuleA();
复制代码

咱们知道浏览器是不认识的 import 语法的,直接在浏览器中运行这样的代码会报错。那么咱们就能够借助 webpack 来打包这样的代码,赋予 JavaScript 模块化的能力。java

最第一版本的 webpack 只能打包 JavaScript 代码,随着发展 css 文件,图片文件,字体文件均可以被 webpack 打包。node

本文将主要讲解 webpack 是如何打包这些资源的,属于比较基础的文章主要是为了后面讲解性能优化和原理作铺垫,若是已经对 webpack 比较熟悉的同窗能够跳过本文。react

webpack 基础功能

初始化安装 webpack

mkdir webpackDemo // 建立文件夹
cd webpackDemo // 进入文件夹
npm init -y // 初始化package.json

npm install webpack webpack-cli -D // 开发环境安装 webpack 以及 webpack-cli 
复制代码

经过这样安装以后,咱们能够在项目中使用 webpack 命令了。webpack

打包第一个文件

webpack.config.jsgit

const path = require('path');

module.exports = {
  mode: 'development', // {1}
  entry: { // {2}
  	main:'./src/index.js'
  }, 
  output: { // {3}
    publicPath:"", // 全部dist文件添加统一的前缀地址,例如发布到cdn的域名就在这里统一添加
    filename: 'bundle.js',
    path: path.resolve(__dirname,'dist')
  }
}
复制代码

代码分析:es6

  • {1} mode 打包模式是开发环境仍是生成环境, development | production
  • {2} entry 入口文件为 index.js
  • {3} output 输出到 path 配置的 dist 文件夹下,输出的文件名为 filename 配置的 bundle.js

[注意] 这个基础的配置文件哪怕你不写,咱们执行 webpack 命令也能够运行,那是由于 webpack 提供了一个默认配置文件。

建立文件进行简单打包:

src/moduleA.js

const moduleA = function () {
  return "moduleA"
}

export default moduleA;

--------------------------------

src/index.js

import moduleA from "./moduleA";

console.log(moduleA());
复制代码

修改 package.json  srcipts

"scripts": {
  "build": "webpack --config webpack.config.js"
}
复制代码

执行 npm run build 命令

image.png

打包后的 bundle.js 源码分析

源码通过简化,只把核心部分展现出来,方便理解

(function(modules) {
 	var installedModules = {};

 	function __webpack_require__(moduleId) {
        // 缓存文件
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
		// 初始化 moudle,而且也在缓存中存入一份
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
		// 执行 "./src/index.js" 对应的函数体
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

		// 标记"./src/index.js"该模块以及加载
 		module.l = true;
    
 		// 返回已经加载成功的模块
 		return module.exports;
 	}
	// 匿名函数开始执行的位置,而且默认路径就是入口文件
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
	// 传入匿名执行函数体的module对象,包含"./src/index.js","./src/moduleA.js"
	// 以及它们对应要执行的函数体
 ({
   "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
   
  }),

   "./src/moduleA.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst moduleA = function () {\n return \"moduleA\"\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (moduleA);\n\n\n//# sourceURL=webpack:///./src/moduleA.js?");

  })

 });

复制代码

再来看看"./src/index.js" 对应的执行函数

(function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
	eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
})
复制代码

你会发现其实就是一个 eval 执行方法

咱们拆开 eval 来仔细看看里面是什么内容,简化后代码以下:

var moduleA = __webpack_require__("./src/moduleA.js");
console.log(Object(moduleA["default"])());
复制代码

上面源码中其实已经调用了 __webpack_require__(__webpack_require__.s = "./src/index.js"); ,而后 "./src/index.js" 又递归调用了去获取 "./src/moduleA.js" 的输出对象。

咱们看看 "./src/moduleA.js" 代码会输出什么:

const moduleA = function () {
  return "moduleA"
}
__webpack_exports__["default"] = (moduleA);
复制代码

再回头看看上面的代码就至关于:

console.log(Object(function () {
  return "moduleA"
})());
复制代码

最后执行打印了 "moduleA"

经过这段源码的分析能够看出:

  1. 打包以后的模块,都是经过 eval 函数进行执行的;
  2. 经过调用入口函数 ./src/index.js 而后递归的去把全部模块找到,因为递归的一个缺点,会进行重复计算,所以 __webpack_require__ 函数中有一个缓存对象 installedModules 来处理这个问题。

loader

咱们知道 webpack 能够打包 JavaScript 模块,并且也早就据说 webpack 还能够打包图片、字体以及 css,这个时候就须要 loader 来帮助咱们识别这些文件了。

[注意] 碰到文件不能识别记得找 loader 便可。

打包图片文件

修改配置文件:webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: { 
    main:'./src/index.js'
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname,'dist')
  },
  module:{
    rules:[
      {
        test:/\.(png|svg|jpg|gif)$/,
        use:{
          loader: 'url-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath:"images", // 打包该资源到 images 文件夹下
            limit: 2048 // 若是图片的大小,小于2048KB则时输出base64,不然输出图片
          }
        }
      }
    ]
  }
}
复制代码

修改:src/index.js

import moduleA from "./moduleA";
import header from "./header.jpg";

function insertImg(){
  const imageElement = new Image();
  imageElement.src = `dist/${header}`;
  document.body.appendChild(imageElement);
}

insertImg();
复制代码

执行打包后,发现能够正常打包,而且 dist 目录下也多出了一个图片文件。

咱们简单分析下:

webpack 自己其实只认识 JavaScript 模块的,当碰到图片文件时便会去 module 的配置 rules 中找,发现 test:/\.(png|svg|jpg|gif)$/  ,正则匹配到图片文件后缀时就使用 url-loader  进行处理,若是图片小于 2048KB (这个能够设置成任意值,主要看项目)就输出 base64 。

打包样式文件

{
  test:/\.scss$/, // 正则匹配到.scss样式文件
    use:[
      'style-loader', // 把获得的CSS内容插入到HTML中
      {
        loader: 'css-loader',
        options: {
          importLoaders: 2, // scss中再次import scss文件,也一样执行 sass-loader 和 postcss-loader
          modules: true // 启用 css module
        }
      },
      'sass-loader', // 解析 scss 文件成 css 文件
      'postcss-loader'// 自动增长厂商前缀 -webket -moz,使用它还须要建立postcss.config.js配置文件
    ]
}
复制代码

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};
复制代码

打包解析:

  1. 当 webpack 遇到 xx.scss 样式文件;
  2. 依次调用 postcss-loader 自动增长厂商前缀 -webket -moz ;
  3. 调用 sass-loader 把 scss 文件转换成 css 文件;
  4. 调用 css-loader 处理 css 文件,其中 importLoaders:2 ,是 scss 文件中引入了其它 scss 文件,须要重复调用 sass-loader postcss-loader 的配置项;
  5. 最后调用 style-loader 把前面编译好的 css 文件内容以 <style>...</style> 形式插入到页面中。

[注意] loader的执行顺序是数组后到前的执行顺序。

打包字体文件

{
  test: /\.(woff|woff2|eot|ttf|otf)$/, // 打包字体文件
  use: ['file-loader'] // 把字体文件移动到dist目录下
}
复制代码

plugins

plugins 能够在 webpack 运行到某个时刻帮你作一些事情,至关于 webpack 在某一个生命周期插件作一些辅助的事情。

html-webpack-plugin

做用:

会在打包结束后,自动生产一个 HTML 文件(也可经过模板生成),并把打包生成的 JS 文件自动引入到 HTML 文件中。

使用:

const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
  new HtmlWebpackPlugin({
    template: 'src/index.html' // 使用模板文件
  })
]
复制代码

clean-webpack-plugin

做用:

每次输出打包结果时,先自动删除 output 配置的文件夹

使用:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

plugins: [
  ...
  new CleanWebpackPlugin() // 使用这个插件在每次生成dist目录前,先删除dist目录
]
复制代码

source map

在开发过程当中有一个功能是很重要的,那就是错误调试,咱们在编写代码过程当中出现了错误,编译后的包若是提示不友好,将会严重影响咱们的开发效率。而经过配置 source map 就能够帮助咱们解决这个问题。

示例: 修改:src/index.js,增长一行错误的代码

console.log(a);
复制代码

因为mode: 'development' 开发模式是默认会打开source map功能的,咱们先关闭它。

devtool: 'none' // 关闭source map 配置
复制代码

执行打包来看下控制台的报错信息:

image.png
错误堆栈信息,居然给的是打包以后的 bundle 文件中的信息,但其实咱们在开发过程当中的文件结构并非这样的,所以咱们须要它能指明咱们是在 index.js 中的多少行发生错误了,这样咱们就能够快速的定位到问题。

咱们去掉 devtool:'none' 这行配置,再执行打包:

image.png
此时它就把咱们在开发中的具体错误文件在错误堆栈中输出了,这就是source map的功能。

总结下:source map 它是一个映射关系,它知道 dist 目录下 bundle.js 文件对应的实际是 index.js 文件中的多少行。

webpackDevServer

每次修改完代码以后都要手动去执行编译命令,这显然是不科学的,咱们但愿是每次写完代码,webpack 会进行自动编译,webpackDevServer 就能够帮助咱们。

增长配置:

devServer: {
  contentBase: './dist', // 服务器启动根目录设置为dist
  open: true, // 自动打开浏览器
  port: 8081 // 配置服务启动端口,默认是8080
},
复制代码

它至关于帮助咱们开启了一个 web 服务,并监听了 src 下文件当文件有变更时,自动帮助咱们进行从新执行 webpack 编译。

咱们在 package.json 中增长一条命令:

"scripts": {
 	"start": "webpack-dev-server"
},
复制代码

如今咱们执行 npm start  命令后,能够看到控制台开始实行监听模式了,此时咱们任意更改业务代码,都会触发 webpack 从新编译。

手动实现简单版 webpack-dev-server

项目根目录下增长:server.js

加载包: npm install express webpack-dev-middleware -D

const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js'); // 引入webpack配置文件
const compiler = webpack(config); // webpack 编译运行时

// 告诉 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件做为基础配置
app.use(webpackDevMiddleware(compiler, {}));

// 监听端口
app.listen(3000,()=>{
  console.log('程序已启动在3000端口');
});
复制代码

webpack-dev-middleware 做用:

  1. 经过 watch mode 监听资源的变动而后自动打包,本质上是调用 compiler 对象上的 watch 方法;
  2. 使用内存文件系统编译速度快 compiler.outputFileSystem = new MemoryFileSystem() ;
  3. 返回 express 框架可用的中间件。

package.json 增长一条命令:

"scripts": {
  "server": "node server.js"
},
复制代码

执行命令 npm run server  启动咱们自定义的服务,浏览器中输入 http://localhost:3000/  查看效果。

热更新 Hot Moudule Replacement(HMR)

模块热更新功能会在应用程序运行过程当中,替换、添加或删除模块,而无需从新加载整个页面。

HMR 配置

const webpack = require('webpack');
module.exports = {
	devServer: {
  	  contentBase: './dist',
  	  open: true,
  	  port: 8081,
  	  hot: true // 热更新配置
	},
	plugins:[
  	new webpack.HotModuleReplacementPlugin() // 增长热更新插件
  ]
}
复制代码

手动编写 HMR 代码

在编写代码时常常会发现热更新失效,那是由于相应的 loader 没有去实现热更新,咱们看看如何简单实现一个热更新。

import moduleA from "./moduleA";

if (module.hot) {
  module.hot.accept('./moduleA.js', function() {
    console.log("moduleA 支持热更新拉");
    console.log(moduleA());
  })
}
复制代码

代码解释: 咱们引人本身编写的一个普通 ES6 语法模块,加入咱们想要实现热更新就必须手动监听相关文件,而后当接收到更新回调时,主动调用。

还记得上面讲 webpack 打包后的源码分析吗,webpack 给模块都创建了一个 module 对象,当你开启模块热更新时,在初始化 module 对象时增长了(源码通过删减):

function hotCreateModule(moduleId) {
  var hot = {
    active: true,
    accept: function(dep, callback){
      if (dep === undefined) hot._selfAccepted = true;
      else if (typeof dep === "function") hot._selfAccepted = dep;
      else if (typeof dep === "object")
      for (var i = 0; i < dep.length; i++)
 	  hot._acceptedDependencies[dep[i]] = callback || function() {};
 	  else hot._acceptedDependencies[dep] = callback || function() {};
    }
  }
}
复制代码

module 对象中保存了监听文件路径和回调函数的依赖表,当监听的模块发生变动后,会去主动调用相关的回调函数,实现手动热更新。

[注意] 全部编写的业务模块,最终都会被 webpack 转换成 module 对象进行管理,若是开启热更新,那么 module 就会去增长 hot 相关属性。这些属性构成了 webpack 编译运行时对象。

编译 ES6

显然你们都知道必需要使用 babel 来支持了,咱们具体看看如何配置

配置

一、安装相关包

npm install babel-loader @babel/core @babel/preset-env @babel/polyfill -D
复制代码

二、修改配置 webpack.config.json

还记得文章上面说过,碰到不认识的文件类型的编译问题要求助 loader

module:{
  rules:[
    {
      test: /\.js$/, // 正则匹配js文件
      exclude: /node_modules/, // 排除 node_modules 文件夹
      loader: "babel-loader", // 使用 babel-loader
      options:{
        presets:[
          [
            "@babel/preset-env", // {1}
           { useBuiltIns: "usage" } // {2}
          ]
        ]
      }
    }
  ]
}
复制代码

babel 配置解析:

  • {1} babel presets 是一组插件的集合,它的做用是转换 ES6+ 的新语法,可是一些新 API 它不会处理的
    • Promise  Generator 是新语法
    • Array.prototype.map 方法是新 API ,babel 是不会转换这个语法的,所以须要借助 polyfill 处理
  • {2} useBuiltIns 的配置是处理 @babel/polyfill 如何加载的,它有3个值 false entry usage
    • false: 不对 polyfills作任何操做;
    • entry: 根据 target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill
    • usage:检测代码中ES6/7/8等的使用状况,仅仅加载代码中用到的polyfills

演示

新建文件 src/moduleES6.js

const arr = [
  new Promise(()=>{}),
  new Promise(()=>{})
];
function handleArr(){
  arr.map((item)=>{
    console.log(item);
  });
}
export default handleArr;
复制代码

修改文件 src/index.js

import moduleES6 from "./moduleES6";
moduleES6();
复制代码

执行打包后的源文件(简化后):

"./node_modules/core-js/modules/es6.array.map.js":
(function(module, exports, __webpack_require__) {
"use strict";
var $export = __webpack_require__("./node_modules/core-js/modules/_export.js");
var $map = __webpack_require__("./node_modules/core-js/modules/_array-methods.js")(1);

$export($export.P + $export.F * !__webpack_require__(/*! ./_strict-method */ "./node_modules/core-js/modules/_strict-method.js")([].map, true), 'Array', {
  map: function map(callbackfn) {
    return $map(this, callbackfn, arguments[1]);
  }
});
复制代码

看代码就应该能明白了 polyfill 至关因而使用 ES5 的语法从新实现了 map 方法来兼容低版本浏览器。

而 polyfill 实现了 ES6-ES10 全部的语法十分庞大,咱们不可能所有引入,所以才会有这个配置 useBuiltIns: "usage" 只加载使用的语法。

编译 React 文件

配置

安装相关依赖包

npm install @babel/preset-react -D
npm install react react-dom
复制代码

webpack.config.js

module:{
  rules:[
    {
      test: /\.js$/, // 正则匹配js文件
      exclude: /node_modules/, // 排除 node_modules 文件夹
      loader: "babel-loader", // 使用 babel-loader
      options:{
        presets:[
          [
            "@babel/preset-env",
           { useBuiltIns: "usage" }
          ],
          ["@babel/preset-react"]
        ]
      }
    }
  ]
}
复制代码

直接在 presets 配置中增长一个 ["@babel/preset-react"]  配置便可, 那么这个 preset 就会帮助咱们把 React 中 JSX 语法转换成 React.createElement 这样的语法。

演示

修改文件:src/index.js

import React,{Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component{
  render(){
    const arr = [1,2,3,4];
    return (
      arr.map((item)=><p>num: {item}</p>)
    )
  }
}

ReactDom.render(<App />, document.getElementById('root')); 复制代码

执行打包命令 yarn build 能够正确打包而且显示正常界面。

随着项目的复杂度增长,babel 的配置也随之变的复杂,所以咱们须要把 babel 相关的配置提取成一个单独的文件进行配置方便管理,也就是咱们工程目录下的 .babelrc 文件。

.babelrc

{
  "presets":[
    ["@babel/preset-env",{ "useBuiltIns": "usage" }],
    ["@babel/preset-react"]
  ]
}
复制代码

[注意] babel-laoder 执行 presets 配置顺序是数组的后到前,与同时使用多个 loader 的执行顺序是同样的。

也就是把 webpack.config.js  中的 babel-loader 中的 options 对象提取成一个单独文件。

image.png
经过编译记录,咱们能够发现一个问题就是打包后的 bundle.js  文件足足有1M大,那是由于 react 以及 react-dom 都被打包进来了,webpack 优化的文章中会讲解如何 code-splitting 进行优化。

小结

本文主要仍是以如何使用 webpack 为主线,让你们对 webpack 有一个初步的印象,而且学会使用 webpack 配置简单的项目,在面试中并不会问这些,所以这篇文章只是一篇铺垫性质的文章,真正面试中文的较多的仍是webpack 的高级用法以及性能优化,更甚至也会问及 webpack 的打包执行原理,在以后的几篇文章会详细讲解。

代码托管地址

相关文章
相关标签/搜索