前端工程化之Webpack---基础篇

446 阅读7分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

引言

webpack作为前端最常用的项目构建工具,但却常常因为其繁琐的配置让人望而却步,这篇文章将从最基础的部分带领大家一步一步配置webpack

Webpack是什么

Webpack是目前最常用的前端资源构建工具,在Webpack看来,前端所有资源文件(.css/js/jsx/img/png/sass...)都会作为模块处理。Webpack根据你的项目结构,将ES6或者其他浏览器不能直接使用的拓展语言,例如typescript、sass、vue等,通过Loaders处理打包成浏览器能读懂的语言,以便在浏览器上能够正确运行

安装与使用

  1. 新建文件夹,使用npm init -y初始化npm
mkdir webpack
cd webpack
npm init -y
  1. 安装webpackwebpack-cli
npm install webpack webpack-cli -D
  1. 新建src文件夹,新建index.htmlindex.js,修改index.js
// index.js
console.log('hello webpack!')
  1. 在控制台输入npx webpack
npx webpack

可以看到在项目根目录下生成了dist文件夹,并且里面有main.js,说明webpack打包成功!

image.png

Webpack基本配置

在项目根目录下新建webpack配置文件webpack.config.js

入口 entry

entry规定了webpack构建的入口文件,它决定了webpack从哪个模块开始生成依赖图

// webpack.config.js
module.exports = {
  // 入口
  entry: {
    main: './src/index.js'
  }
}

多个入口配置:

// webpack.config.js
module.exports = {
  // 入口
  entry: {
    one: './src/one.js',
    two: './src/two.js'
  }
}

// 或
module.exports = {
  // 入口
  entry: {
    main: [
      './src/one.js',
      './src/two.js'
    ]
  }
}

出口 output

output字段规定webpack应该在哪里输出它构建的文件path,以及如何命名文件filename

// webpack.config.js
const path = require('path')

module.exports = {
  // 入口
  entry: {
    main: './src/index.js'
  },
  // 出口
  output: {
    path: path.resolve(__dirname, 'dist'),    // ./dist
    filename: 'main.js'
  } 
}

模式 mode

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。

  1. development 会将process.env.NODE_ENV的值设为development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
module.exports = {
  // ...
  mode: 'development'
}
  1. production 会将process.env.NODE_ENV的值设为production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin...
module.exports = {
  // ...
  mode: 'production'
}

也可以在运行打包命令时规定打包模式

npx webpack --mode developmenet

Loader

Loaders是webpack的核心概念之一,由于webpack默认只能处理js文件,那么在遇到其他文件类型css/jpg/png/ts...时,需要通过不同的Loader处理不同类型的文件,例如将ES6转化为浏览器能够读懂的ES5,将TypeScript转化成JavaScript,将sass转化为css...它有点类似于工厂流水线上的机器,将生产原料一步步处理打包,最终输出能够使用的产品

Loaders的配置只有简单的几项(*为必填项):

  • test:匹配对应文件格式的正则表达式(*)
  • use:使用的loader(*)
  • include/exclude:需要处理的文件/不需要处理的文件

使用file-loader打包jpg/png/gif图片

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: 'file-loader',
          options: {    // 为loader提供额外的配置(可选)
            // 占位符[]
            name: '[name].[ext]',    // 使用原来的文件名和后缀
            // 也可以为 [name]_[hash].[ext] 文件名_哈希值
            outputPath: 'images/'    // 输出路径
          }
        }
      }
    ]
  }
}

运行npx webpack可以看到dist目录下出现了图片

使用url-loader打包图片

url-loader会将图片格式转化为Base64格式

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name].[ext]',    // 使用原来的文件名和后缀
            outputPath: 'images/',
            limit: 2048    // 限制文件不得超过2048字节,也就是2KB
            // 如果超过,就使用file-loader
          }
        }
      }
    ]
  }
}

假如我们想把图片的样式修改宽高为300px,通常做法是新建index.css文件,写入样式后在index.js文件中引入

.avatar {
  width: 300px;
  height: 300px;
}

index.js文件中引入

import image from './psc.jpg'
import './index.css'

const img = new Image()
img.src = image
img.className += 'avatar'

const app = document.getElementById('app')
app.appendChild(img)

此时我们运行npx webpack可以发现控制台报错:

image.png 这是因为webpack无法解析.css文件,这时候我们需要相应的Loader来对此类型文件进行处理CSS文件

使用css-loader和style-loader打包样式

安装css-loaderstyle-loader

npm install css-loader style-loader -D
// webpack.config.js
{
  test: /.css$/,
  use: [
    'style-loader',
    'css-loader',
    'postcss-loader',
    'sass-loader'
  ]
}

注意:plugin数组的执行顺序是 从下往上 / 从右往左 依次处理

使用sass-loader处理scss文件

npm install sass sass-loader -D
body {
  .avatar {
    width: 300px;
    height: 300px;
  }
}

执行顺序:sass-loader先将sass文件转换为css文件,再交给css-loaderstyle-loader

module.exports = {
  // ...
  {
    test: /.scss$/,
    use: [
      'style-loader',
      'css-loader',
      'sass-loader'
    ]
  }
}

为css3属性加上浏览器前缀

对于一些CSS3属性,我们希望给它加上对应浏览器的前缀(例如-webkit-),来保证它能在各个浏览器中正确运行。postcss-loader能够为我们自动加上浏览器前缀

npm install postcss postcss-loader autoprefixer -D
// webpack.config.js
// ...
{
  test: /.css$/,
  use: [
    'style-loader',
    'css-loader',
    'postcss-loader',
    'sass-loader'
  ]
}

新建postcss.config.js

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')  // 引入autoprefixer插件
  ]
}

这个插件内部会按照package.json中的browserslist属性来添加对应前缀,因此我们需要对package.json加入相应配置

// package.json
"browserslist": [
  "> 1%", // 需要兼容市场份额百分之1的浏览器
  "last 2 versions"   // 需要兼容上两个版本
]

Babel处理ES6

安装loader

npm install -D babel-loader @babel/core @babel/preset-env

注意:这里有三个模块需要安装,不要漏了=.=

const path = require('path')

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

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/'
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin()
  ]
}

打包输出后的main.js/bundle.js

image.png

可以看到,这里有一个问题:Babel只能转换ES6语法,而不能转换新的API,比如Generator、Set、Maps、Proxy、Symbol、Promise、Async等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转化。对于这些,我们可以用babel-polyfill来处理

Babel处理ES6新api

安装(注意要用--save,因为线上环境也需要用到):

npm install @babel/polyfill --save

index.js文件中引入babel-polyfill

import '@babel-polyfill'

打包输出后的main.js image.png 可以看到main.js中多出了很多代码,打包完成的js文件大小为405KB,而我们在使用babel-polyfill打包前的代码大小只有264个字节(Bytes),为什么两次打包的文件大小相差这么大呢? image.png 原因是webpackbabel-polyfill整体全部都打包进去了。而babel-polyfill肯定也实现了所有ES6新API的垫片,文件一定不会小。

我们可以在babel-loader中加入useBuiltIns配置,告诉babel-polyfill只需要将使用到的es6代码进行打包,以避免不必要的打包,减少打包输出体积。

// webpack.config.js
modules.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  useBuiltIns: 'usage'
                }
              ]
            ]
          }
        }
      }
    ]
  }
}

输入命令后可以看到打包输出的代码大小只有192KB大小,明显小了不少 image.png

Plugin

Plugin是webpack另一个核心的概念,它可以在webpack运行到某个阶段的时候拓展webpack的功能。这里推荐两个常用的plugin

  1. html-webpack-plugin html-webpack-plugin在打包完毕之后自动帮我们生成html文件,并将打包后的结果注入到html中。

安装

npm install html-webpack-plugin -D

使用

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

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: 'file-loader',
          options: {    // 为loader提供额外的配置(可选)
            // 占位符[]
            name: '[name].[ext]',    // 使用原来的文件名和后缀
            // 也可以为 [name]_[hash].[ext] 文件名_哈希值
            outputPath: 'images/'    // 输出路径
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

接收模板配置

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

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /.(jpg|png|gif)$/,
        use: ['file-loader']
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

这里我们让打包后引入index.js文件的过程自动在index.html中注入

  1. clean-webpack-plugin 每次在npm run build之前我们都需要手动删除之前的dist文件夹,clean-webpack-plugin可以帮助我们在输出打包完的文件前,自动先删除之前生成的dist文件夹

安装

npm install clean-webpack-plugin -D

使用

const path = require('path')

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

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin()
  ]
}

SourceMap

浏览器在实际运行时执行的是经webpack处理后的文件main.jsbundle.js,那么问题来了,如果代码中出现错误,浏览器在控制台中报错指向的是打包后的代码main.js,也就是说如果我们在源代码index.js的第5行出了错,在打包后的代码中不知道指向的是第几行,由此SourceMap应运而生,SourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,它可以将打包后的代码准确指向到源代码对应的位置上

配置SourceMap

// 开发环境
module.exports = {
  mode: 'development',
  devtool: 'cheap-module-source-map',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin()
  ]
}

WebpackDevServer

在每次修改代码后,我们都需要重新运行npx webpack才能看到修改后的效果,那么在webpack中有没有什么办法能够为我们在修改代码后保存自动进行打包呢?webpack-dev-server可以在本地启动一个服务器,并让打包输出的文件运行在服务器上,这样一来,我们在修改了代码保存后,webpack-dev-server会自动为我们进行打包

安装

npm install webpack-dev-server -D

package.json中加入以下命令

// package.json
"scripts": {
  "watch": "webpack --watch",
  "start": "webpack serve"
}

运行npm run start启动webpack服务器,并运行在localhost:8080

open:自动打开浏览器

// webpack.config.js
devServer: {
  contentBase: './dist',    // webpack-dev-server3
  open: true
}

运行npm run start后可以看到webpack自动为我们打开浏览器并访问localhost:8080

使用WebpackDevServer实现请求转发

在开发过程中经常需要用到http请求,proxy可以在你请求接口时,将请求转发到别的url上

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    contentBase: './dist',
    proxy: {
      '/api': {
        target: 'http://localhost:3000'
      }
    }
   }
}
axios.get('/api/users').then(res => {
  // ...
})

这个时候,如果发送请求到 /api/users,会被代理到请求 http://localhost:3000/api/users

HMR 模块热替换

不需要刷新页面,保存即更新。也就是说已经渲染到页面上的内容不会消失,而是自动根据修改的代码直接应用到页面上

const path = require('path')

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

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  devserver: {
    contentBase: './dist',
    open: true,
    hot: true    // 开启HMR
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /.(jpg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/'
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new CleanWebpackPlugin()
  ]
}

注意:在Webpack4中HMR是默认开启的

总结

本文详细介绍了以下webpack的一些基础配置,希望能够帮助前端小白入门webpack,webpack配置的过程看起来非常复杂,从webpack官网也能看到配置项也非常的多,但是不要被它表面的繁琐吓住了,主要理清楚webpack的逻辑,一步一步配置好对应项,其实webpack也就(zhen)这(de)样(nan)

243118cd379d682b48a5a7bf35282c4.jpg

最后,如果本文对你有帮助的话,留下一个赞吧~