Skip to content
大纲

Webpack 4

本文主要记录我在学习 Webpack 4 过程中所遇到的一些知识点,以及自己写的一些 demo

参考资料:Webpack 官方文档

初识 Webpack

Webpack 是什么?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

Webpack 的四个概念

  1. 入口 entry
  2. 输出output
  3. loader
  4. 插件 plugins

在项目中安装 Webpack

  1. 使用 npm 初始化项目:npm init
  2. 安装 webpackwebpack-cli(开发环境):npm i webpack webpack-cli --save-dev
  3. 如果不配置任何东西,执行打包命令:npx webpack
  4. 默认 webpack 会将 src 目录下的 js 文件打包到 dist 目录下的 main.js 文件,打包后的文件会合并压缩 js 代码,模块化的 js 也能被转换成浏览器能运行的 js

Webpack 配置文件

在项目根目录下创建 webpack.config.js 文件,即 webpack 的配置文件,后续所有的配置都将在此文件中编写:

javascript
// webpack.config.js

// webpack 是 node 写出来的,是 node 的语法
// 引入 path 路径模块
const path = require('path')
// 导出配置对象
module.exports = {
 entry: './src/index.js', // 入口,要打包的文件
 output: {
  // 出口,打包后的文件
  filename: 'bundle.js', // 打包后的文件名
  path: path.resolve(__dirname, 'dist') // 打包后的目录路径,必须是一个绝对路径,使用 path.resolve() 方法将指定的目录名解析成绝对路径
 },
 mode: 'development' // 模式 默认两种 production(生产模式 压缩代码) development(开发模式 未压缩代码)
}

也可以不使用 webpack.config.js 作为 webpack 的配置文件,只需:

  1. 创建一个 webpack.myconfig.js 自定义 webpack 配置文件
  2. 在运行时输入 npx webpack --config webpack.myconfig.js

还可以在 package.json 中配置自定义的 webpack 文件:

  1. scripts 中配置:
json
"scripts"{
    "build": "webpack --config webpack.myconfig.js"
  }
  1. 使用 npm run build 即可进行打包

Webpack 常用 module、loader 和 plugin

常用 loader

  • babel 相关
    1. babel-loader
    2. @babel/core
    3. @babel/env
    4. @babel/preset-env
    5. @babel/preset-react
    6. @babel/runtime
    7. @babel/plugin-transform-runtime
    8. @babel/preset-typescript
  • cssscssless 相关
    1. css-loader
    2. style-loader
    3. sass-loader
    4. less-loader
    5. node-sass
    6. dark-sass
    7. vue-style-loader
    8. MiniCssExtractPlugin.loader
  • 静态资源相关
    1. url-loader 处理图片
    2. file-loader 处理其他静态资源
    3. html-loader 处理 html 中的图片
  • typescript 相关
    1. ts-loader
    2. awesome-typescript-loader
  • vue 相关
    1. vue-loader
    2. vue-template-compiler
    3. vue
  • react 相关
    1. react
    2. react-dom
    3. @babel/preset-react
    4. @babel/core
    5. babel-loader

常用 module 和 plugin

  1. webpack webpack-cli 总的打包模块
  2. webpack-dev-server http 服务器
  3. html-webpack-plugin 将 Js 打包到指定的 html 中
  4. clean-webpack-plugin 打包前清空 dist 或者指定的目录
  5. copy-webpack-plugin 复制指定的文件到打包后的目录中
  6. webpack.BannerPlugin 在每个打包后的文件顶部加上版权信息(内置插件,无需安装)
  7. progress-bar-webpack-plugin 打包进度条
  8. mini-css-extract-plugin 将 css 从 JS 中分离出来
  9. webpack-dev-middleware 从服务端启动 webpack,解决跨域
  10. webpack-merge 合并 webpack 公共配置
  11. optimize-css-assets-webpack-plugin 压缩 css
  12. happypack 多线程打包

Webpack 基本配置

webpack-dev-server 开发服务器

webpack 的插件 webpack-dev-server 可以启动一个本地的 http 服务器,然后在打包好的目录下运行打包后的项目。
安装 webpack-dev-server : npm i webpack-dev-server --save-dev
webpack.config.js 配置文件中配置 http 服务器:

javascript
module.exports = {
 devServer: {
  // 开发环境 http 服务器配置
  port: 4000, // 端口号
  progress: true, // 加载进度条
  contentBase: './dist' // 服务器主目录,会自动加载该目录下的 index.html 文件
 }
}

启动服务器方式:

  1. 在终端输入 npx webpack-dev-server 启动服务器
  2. package.json scripts 中配置启动命令 "server" : "webpack-dev-server"
  3. 终端输入 npm run server 启动服务器

打包静态资源

tip: 下面代码中使用到的loaderplugin需要通过 npm install -D ... 进行安装(...为模块名)

javascript
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
 entry: './src/main.js',
 output: {
  path: resolve(__dirname, 'dist'),
  filename: '[name].js'
 },
 module: {
  rules: [
   {
    test: /\.css$/,
    use: ['style-loader', 'css-loader']
   },
   {
    test: /\.(sass|scss)$/,
    use: ['style-loader', 'css-loader', 'sass-loader']
   },
   {
    test: /\.(png|jpg|gif)$/,
    use: [
     {
      loader: 'url-loader',
      options: {
       limit: 10000,
       name: '[name][hash:10].[ext]',
       outputPath: 'images/'
       // esModule: false // 关闭es 模块,使用 commjs 模块
      }
     }
    ]
   },
   // 处理html中的图片
   {
    test: /\.html$/,
    use: [
     {
      loader: 'html-loader',
      options: {
       outputPath: 'images/'
      }
     }
    ]
   },
   {
    exclude: /\.(js|css|sass|scss|png|jpg|gif|html)$/,
    use: [
     {
      loader: 'file-loader',
      options: {
       outputPath: 'static/'
      }
     }
    ]
   }
  ]
 },
 plugins: [
  new HtmlWebpackPlugin({
   template: './public/index.html',
   inject: 'body'
  })
 ],
 mode: 'development',
 // 需要安装 webpack-dev-server
 devServer: {
  contentBase: 'dist', // 输入目录,(在内存中,不会实际输出)
  compress: true, // 开启gzip压缩
  port: 9000, // 端口号
  open: true // 自动打开浏览器
 }
}

Webpack 进阶配置

resolve 属性配置

这些选项能配置模块如何被解析。

javascript
// 配置模块如何被解析
resolve: {
 // 告诉 webpack 解析模块时应该搜索的目录。
 modules: [resolve(__dirname, 'node_modules')],

 // 创建 import 或 require 的别名,来确保模块引入变得更简单
 alias: {
  bootstrap: 'bootstrap/dist/css/bootstrap.css'
 },

 // 定义找第三方模块 package.json 中入口文件的字段名,值为数组,按照从左到右的顺序寻找
 mainFields: ['style', 'main'], // 先找 package.json/style 字段,找不到再找 main 字段

 // 指定要解析的扩展名,在引入文件时便可以不加扩展名(比如vue在引入组件时通常不加扩展名);值为数组,会按照从左到右的优先级去自动添加扩展名
 extensions: ['.js', '.css', '.json']
},

区分不同环境

使用 webpack.DefinePlugin 插件定义环境变量

javascript
/* ------- webpack.config.js -------- */

// 定义环境变量
const { DefinePlugin } = require('webpack')

module.exports = {
 plugins: [
  // 配置 ENV 环境变量
  new DefinePlugin({
   ENV: JSON.stringify('development')
  })
 ]
}
/* ------- webpack.config.js -------- */

/* ------ src/index.js ------- */

// 使用环境变量
let url = ''

if (ENV && ENV === 'development') {
 url = '开发环境地址'
} else {
 url = '生产环境地址'
}
console.log(url) // 开发环境地址

/* ------ src/index.js ------- */

单独创建开发环境和生产环境 Webpack 配置

需要创建主配置、生产环境配置、开发环境配置三个 webpack 配置文件,然后使用 webpack-merge 模块合并主配置文件到其他两个环境文件中;最后在 package.json/scripts 中配置启动/打包时指定对应的配置文件即可
webpack.config.base.js 主配置文件:

javascript
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

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

webpack.config.dev.js 开发环境配置文件:

javascript
// 开发环境配置

const { DefinePlugin } = require('webpack')
// 合并配置
const { merge } = require('webpack-merge')
// 公用配置
const base = require('./webpack.config.base')

module.exports = merge(base, {
 mode: 'development',
 plugins: [
  // 配置环境变量
  new DefinePlugin({
   ENV: JSON.stringify('development')
  })
 ]
})

webpack.config.prod.js 生产环境配置文件:

javascript
// 生产环境配置

const { DefinePlugin } = require('webpack')
// 合并配置
const { merge } = require('webpack-merge')
// 公用配置
const base = require('./webpack.config.base')

module.exports = merge(base, {
 mode: 'production',
 plugins: [
  // 配置环境变量
  new DefinePlugin({
   ENV: JSON.stringify('production')
  })
 ]
})

package.json/scripts 脚本配置:

javascript
"scripts": {
  "build": "webpack --config webpack.config.prod.js",
  "start": "webpack-dev-server --config webpack.config.dev.js"
 },

src/index.js 使用环境变量:

javascript
let url = ''

if (ENV && ENV === 'development') {
 url = '开发环境地址'
} else {
 url = '生产环境地址'
}

// 如果是通过 start 命令启动,打印 "开发环境地址";使用 build 打包后启动则打印 "生产环境地址"
console.log(url)

Webpack 跨域问题

通过 webpack-dev-server 配置代理

javascript
// webpack-dev-server 解决跨域问题
devServer: {
    // 代理
    proxy: {
     // 将 localhost:8080/api 开头的请求代理到 localhost:3000/api
     '/api': {
      target: 'http://localhost:3000/',
      pathRewrite: { '/api': '' } // 代理时将 /api 去除,去除后:localhost:8080/api/user -> localhost:3000/user
     }
    }
}

前端模拟数据

javascript
// webpack-dev-server 解决跨域问题
devServer: {
 open: true,
 // 2) 前端自己模拟数据
 before(app) {
  // app 就是 express 的实例
  app.get('/api/user', (req, res) => res.json({ name: 'qxc' }))
 }
}

代理部分接口,手写部分接口

如果代理的接口后端也有,那么将有限访问本地的接口,而不会访问后端接口,就近原则

javascript
// webpack-dev-server 解决跨域问题
devServer: {
 open: true,
 // 1) 代理
 proxy: {
  // 将 localhost:8080/api 开头的请求代理到 http://localhost:3000/api
  '/api': {
   target: 'http://localhost:3000/',
   pathRewrite: { '/api': '' } // 代理时将 /api 去除,去除后:localhost:8080/api/user -> http://localhost:3000/user
  }
 },
 // 2) 前端自己模拟数据
 before(app) {
  // app 就是 express 的实例
  app.get(
   '/api/login',
   (req, res) => res.json({ name: 'loginSuccess' }) // 如果后端也有 /login 接口,那么将不会请求后端的接口,就近原则
  )
 }
}

服务端启动 Webpack

Webpack 和服务器同时启动,前端后在一个端口运行,理所当然不会跨域
需要安装 webpack-dev-middleware
基于 express 服务器启动 webpack + 后端服务:

javascript
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')

// 创建 express 服务器实例
const app = express()

// 引入 webpack 配置
const config = require('./webpack.config.js')

// 编译 webpack 配置
const compiler = webpack(config)

// 以 express 中间件的形式启动 webpack
app.use(webpackDevMiddleware(compiler))

// express 接口
app.get('/api/user', (req, res) => res.json({ name: 'qxc' }))
app.get('/api/login', (req, res) => res.json({ name: 'loginBackend' }))

const port = 3000
app.listen(port, () => console.log(`Example app listening on port port!`))

/*
注意:
    1. 使用 webpack 中间件后会占用 / 根路由,在根路由映射前端资源;因此不能再自己处理 / 了
    2. webpack.config.js 中的路径要使用绝对路径(path.resolve),不能使用相对路径
*/

watch 实时打包

watch 属性用于监听,在代码做出修改时触发 webpack 的再次打包
要使用监听功能,首先要开启 watch: true

javascript
watch: true, // 开启监听
watchOptions: {
 // 监听的配置项
 poll: 500, // 每秒检查修改的次数(每秒检查500次)
 aggregateTimeout: 1000, // 防抖,将指定 ms(毫秒) 内做出的多次修改只打包一次
 ignored: /node_modules/ // 屏蔽不需要监控的文件
},

配置 source-map

source-map 是一种源码映射技术,作用是打包的后文件运行时如果出现报错,会将报错信息定位到源码文件中具体的行和列,便于开发人员调试

官方文档地址: https://www.webpackjs.com/configuration/devtool/

属性名为 devtool,有以下取值:

  1. source-map 报错直接定位到源文件具体行和列,打包后生成 sourcemap 文件
  2. inline-source-map 报错直接定位到源文件具体行和列,打包后不会生成 sourcemap 文件
  3. eval-source-map 报错直接定位到源文件具体行和列,打包后不会生成 sourcemap 文件
  4. hidden-source-map
  5. cheap-source-map
  6. cheap-module-source-map
  7. eval
javascript
devtool: "source-map", // enum
devtool: "inline-source-map", // 嵌入到源文件中
devtool: "eval-source-map", // 将 SourceMap 嵌入到每个模块中
devtool: "hidden-source-map", // SourceMap 不在源文件中引用
devtool: "cheap-source-map", // 没有模块映射(module mappings)的 SourceMap 低级变体(cheap-variant)
devtool: "cheap-module-source-map", // 有模块映射(module mappings)的 SourceMap 低级变体
devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快。
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的 `source-map' 是最详细的。

Webpack 打包多页应用

webpack.config.js 配置打包多页应用:

javascript
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
 // 多入口
 entry: {
  home: './src/index.js',
  other: './src/other.js',
  common: './src/common.js'
 },
 output: {
  path: resolve(__dirname, 'dist'),
  // 多出口 [name] home other common
  filename: '[name].js'
 },
 mode: 'development',
 plugins: [
  // new 多次,生成多个页面
  new HtmlWebpackPlugin({
   template: './src/index.html',
   inject: 'body',
   filename: 'home.html',
   // 要引入的打包后的 js,common是公共代码块
   chunks: ['home', 'common']
  }),
  new HtmlWebpackPlugin({
   template: './src/index.html',
   inject: 'body',
   filename: 'other.html',
   chunks: ['other', 'common']
  })
 ]
}

Webpack 3 个小插件应用

  1. clean-webpack-plugin 打包前先删除构建目录,保证构建后的目录是干净的
  2. copy-webpack-plugin 将源码中指定的文件原封不动地拷贝到打包后的目录中
  3. webpack.BannerPlugin 在打包后的每个文件头部加上指定的注释,多用于版权声明(webpack 自带,无需安装)
javascript
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { BannerPlugin } = require('webpack')

module.exports = {
 plugins: [
  // 在打包前先删除 dist 目录(也可传入自己指定的目录)
  new CleanWebpackPlugin(),

  // 将源码中指定的文件原封不动地拷贝到打包后的目录中
  new CopyWebpackPlugin({
   patterns: [{ from: './src/description.txt' }]
  }),

  // 版权声明,默认打包后会将传入的字符串生成版权声明文件
  new BannerPlugin('make by qxc')
 ]
}

Demos

下面示例的完整代码可以在这里找到:https://gitee.com/qiuxchao/webpack-demos

打包 Typescript

javascript
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
 entry: './src/index.ts',
 output: {
  path: resolve(__dirname, 'dist'),
  filename: 'boundle.js'
 },
 module: {
  rules: [
   {
    test: /\.ts$/,
    use: ['ts-loader']
   }
  ]
 },
 plugins: [
  new HtmlWebpackPlugin({
   template: './public/index.html',
   inject: 'body'
  })
 ],
 mode: 'development'
}

打包 Vue

webpack.config.js:

javascript
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const ProgressPlugin = require('progress-bar-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { resolve } = require('path')
module.exports = {
    // 入口
    entry: './src/index.js',
    // 出口
    output: {
        filename: 'boundle.js',
        path: resolve(__dirname, 'dist')
    },
    // loader
    module: {
        rules: [
            // es6 转 es5
            {
                test: /\.js$/,
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            },
            // 处理ts
            {
                test: /\.ts$/,
                loader: 'ts-loader'
            },
            // 处理vue
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            // 处理less
            {
                test: /\.less$/,
                use: ['vue-style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
            },
            // 处理css
            {
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader']
            },
            // 处理图片资源
            {
                test: /\.(jpg|png|gif)$/,
                loader: 'url-loader',
                options: {
                    limit: 8 * 1024,
                    outputPath: 'images/',
                    name: '[name]-[hash:10].[ext]',
                    esModule: false // 关闭es6模块化
                }
            },
            // 处理html中引入的图片资源
            {
                test: /\.(htm|html)$/,
                loader: 'html-loader'
            },
            // 处理其他静态资源
            {
                exclude: /\.(html|htm|js|css|vue|less|jpg|png|gif)$/,
                loader: 'file-loader',
                options: {
                    name: '[name]-[hash:5].[ext]'
                }
            }
        ]
    },
    // 插件
    plugins: [
        new VueLoaderPlugin(),
        new ProgressPlugin(),
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: 'index.css'
        })
        new HtmlWebpackPlugin({
            template: './public/index.html',
            inject: 'body'
        }),
    ],
    // 开发服务器
    devServer: {
        contentBase: resolve(__dirname, 'dist'),
        compress: true,
        port: 8899,
        open: true
    },
    resolve: {
        alias: {//修改vue被导入时候包的路径
            "vue$": "vue/dist/vue.js"
        }
    }
}

package.json:

json
{
 "name": "demo5",
 "version": "1.0.0",
 "main": "index.js",
 "license": "MIT",
 "scripts": {
  "start": "webpack-dev-server --mode=development",
  "build": "webpack --mode=production"
 },
 "devDependencies": {
  "html-loader": "0.5.5",
  "html-webpack-plugin": "4",
  "less-loader": "5",
  "ts-loader": "8",
  "vue-loader": "15.4.1",
  "vue-template-compiler": "2.6.12",
  "webpack": "4",
  "webpack-cli": "3",
  "webpack-dev-server": "^3.11.2"
 },
 "dependencies": {
  "typescript": "^4.2.4",
  "vue": "^2.6.12"
 }
}

打包 React

webpack.config.js:

javascript
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
 entry: './src/index.js',
 output: {
  path: resolve(__dirname, 'dist'),
  filename: 'js/[name].js'
 },
 module: {
  rules: [
   {
    test: /\.jsx?$/,
    loader: 'babel-loader',
    options: {
     presets: ['@babel/preset-react'],
     plugins: ['@babel/plugin-proposal-class-properties']
    }
   },
   {
    test: /\.tsx?$/,
    loader: 'awesome-typescript-loader'
   },
   {
    test: /\.scss$/,
    use: ['style-loader', 'css-loader', 'sass-loader']
   }
  ]
 },
 plugins: [
  new HtmlWebpackPlugin({
   template: './src/index.html',
   inject: 'body'
  })
 ],
 mode: 'development',
 devtool: 'inline-source-map',
 devServer: {
  contentBase: resolve(__dirname, 'dist'),
  compress: true,
  port: 8089,
  open: true
 }
}

package.json:

json
{
 "name": "build_react",
 "version": "1.0.0",
 "main": "index.js",
 "license": "MIT",
 "scripts": {
  "build": "webpack --mode=production",
  "start": "webpack-dev-server --mode=development"
 },
 "devDependencies": {
  "@babel/preset-react": "^7.13.13",
  "node-sass": "4",
  "react": "^17.0.2",
  "react-dom": "^17.0.2",
  "webpack": "4",
  "webpack-cli": "3"
 },
 "dependencies": {
  "awesome-typescript-loader": "^5.2.1",
  "html-webpack-plugin": "4.0.0-alpha.2",
  "sass-loader": "7",
  "webpack-dev-server": "3"
 }
}