Webpack入门 - 从0开始搭建项目配置

前言

webpack 做为前端最知名的打包工具,可以把散落的模块打包成一个完整的应用,大多数的知名框架 cli 都是基于 webpack 来编写。这些 cli 为使用者预设好各类处理配置,使用多了就会以为理所固然,也就不在乎是内部是如何配置。若是脱离 cli 开发,可能就无从下手了。css

最近在开发一些单页项目时,出于需求便开始从头搭建项目配置,本文主要分享搭建时用到的配置。html

准备工做

快速生成 package.json:前端

npm init -y
复制代码

必不可少的 webpack 和 webpack-cli:node

npm i webpack webpack-cli -D
复制代码

入口、出口

webpack 的配置会统一放到配置文件中去管理,在根目录下新建一个名为 webpack.config.js 的文件:webpack

const path = require('path')

module.exports = {
  entry: {
    main: './src/js/main.js'
  },
  output: {
    // filename 定义打包的文件名称
    // [name] 对应entry配置中的入口文件名称(如上面的main)
    // [hash] 根据文件内容生成的一段随机字符串
    filename: '[name].[hash].js',
    // path 定义整个打包文件夹的路径,文件夹名为 dist
    path: path.join(__dirname, 'dist')
  }
}
复制代码

entry 配置入口,可配置多个入口。webpack 会从入口文件开始寻找相关依赖,进行解析和打包。es6

output 配置出口,多入口对应多出口,即入口配置多少个文件,打包出来也是对应的文件。web

修改 package.json 的 script 配置:npm

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

这样一个最简单的配置就完成了,经过命令行输入 npm run build 就能够实现打包。json

配置项智能提示

webpack 的配置项比较繁杂,对于不熟悉的同窗来讲,若是在输入配置项可以提供智能提示,那开发的效率和准确性会大大提升。浏览器

默认 VSCode 并不知道 webpack 配置对象的类型,经过 import 的方式导入 webpack 模块中的 Configuration 类型后,在书写配置项就会有智能提示了。

/** @type {import('webpack').Configuration} */
module.exports = {

}
复制代码

环境变量

通常开发中会分开发和生产两种环境,而 webpack 的一些配置也会随环境的不一样而变化。所以环境变量是很重要的一项功能,使用 cross-env 模块能够为配置文件注入环境变量。

安装模块 cross-env

npm i cross-env -D
复制代码

修改 package.json 的命令传入变量:

"scripts": {
  "build": "cross-env NODE_ENV=production webpack --config webpack.config.js"
}
复制代码

在配置文件里,就能够这样使用传入的变量:

module.exports = {
  devtool: process.env.NODE_ENV === 'production' ? 'none' : 'source-map'
}
复制代码

同理,在项目的 js 内也可使用该变量。

设置 source-map

该选项能设置不一样类型的 source-map 。代码通过压缩后,一旦报错不能准肯定位到具体位置,而 source-map 就像一个地图, 可以对应源代码的位置。这个选项可以帮助开发者加强调试过程,准肯定位错误。

为了体验它的做用,我在源代码中故意输出一个不存在的变量,模拟线上错误:

在预览时,触发错误:

很明显错误的行数是不对应的,下面设置 devtool 让 webpack 在打包后输出 source-map 文件,用于定位错误。

module.exports = {
  devtool: 'source-map'
}
复制代码

再次触发错误,source-map 文件起做用准肯定位到代码错误的行数。

source-map 通常只在开发环境用于调试,上线时绝对不能带有 source-map 文件,这样会暴露源代码。下面经过环境变量来正确设置 devtool 选项。

module.exports = {
  devtool: process.env.NODE_ENV === 'development' ? 'source-map' : 'none'
}
复制代码

devtool 的可选项不少,它的品质和生成速度有关联,好比只定位到某个文件,或者定位到某行某列,相应的生成速度会快或更慢。能够根据你的需求进行选择,更多可选值请查看 webpack 文档

loader 与 plugin

loader 与 plugin 是 webpack 的灵魂。若是把 webpack 比做成一个食品加工厂,那么 loader 就像不少条流水线,对食品原料进行加工处理。plugins 则是在原有的功能上,添加其余功能。

loader 基本用法

loader 配置在 module.rules 属性上。

module.exports = {
  module: {
    rules: []
  }
}
复制代码

下面来简单了解下 loader 的规则。通常经常使用的就是 testuse 两个属性:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪一个 loader。
rules: [
  {
    test: /\.css$/,
    use: ['css-loader']
  }
]
// 也能够写为
rules: [
  {
    test: /\.css$/,
    loader: 'css-loader'
  }
]
复制代码

上面例子是匹配以 .css 结尾的文件,使用 css-loader 解析。

当有些 loader 可传入配置时,能够改成对象的形式:

rules: [
  {
    test: /\.css$/,
    user: [
      {
        loader: 'css-loader',
        options: {}
      }
    ]
  }
]
复制代码

最后须要记住多个 loader 的执行是严格区分前后顺序的,从右到左,从下到上。

plugin 基本用法

plugin 配置在 plugins 属性上。

module.exports = {
  plugins: []
}
复制代码

通常 plugin 会暴露一个构造函数,经过 new 的方式使用。plugin 的参数则在调用函数时传入。plugin 命名通常是将按照原名转为大驼峰。

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

module.exports = {
  plugin: [
    new CleanWebpackPlugin()
  ]
}
复制代码

生成 html 文件

没有通过任何配置的 webpack 打包出来只有 js 文件,使用插件 html-webpack-plugin 能够自定义一个 html 文件做为模版,最终 html 会被打包到 dist 中,而 js 也会被引入其中。

安装 html-webpack-plugin:

npm i html-webpack-plugin -D
复制代码

配置 plugins:

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

module.exports = {
  plugins: [
    // 使用html模版
    new HtmlWebpackPlugin({
      // 配置html标题
      title: 'home',
      // 模版路径
      template: './src/index.html',
      // 压缩
      minify: true
    })
  ]
}
复制代码

此时想要配置的标题生效还须要在 html 中为 title 标签插值:

<title><%= htmlWebpackPlugin.options.title %></title>
复制代码

除了 title 外,还能够配置诸如 meta 标签等。

多页面

上面的使用方法,在打包后只会有一个 html。对于多页面的需求其实也很简单,有多少个页面就 new 几回 htmlWebpackPlugin

可是须要注意一点,入口配置的 js 是会被所有引入到 html 的。若是想某个 js 对应某个 html,能够配置插件的 chunks 选项。并且须要配置 filename 属性,由于 filename 属性默认是 index.html,同名会被覆盖。

module.exports = {
  entry: {
    main: './src/js/main.js',
    news: './src/js/news.js'
  },
  output: {
    filename: '[name].[hash].js',
    path: path.join(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      minify: true,
      chunks: ['main']
    }),
    new HtmlWebpackPlugin({
      template: './src/news.html',
      filename: 'news.html',
      minify: true,
      chunks: ['news']
    }),
  ]
}
复制代码

es6转es5

安装 babel 的相关模块:

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

@babel/core 是 babel 的核心模块,@babel/preset-env 内预设不一样环境的语法转换插件,默认将 es6 转 es5。

根目录下建立 .babelrc 文件:

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

配置 loader:

rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/, 
    loader: 'babel-loader'
  }
]
复制代码

解析 css

安装 loader:

npm i css-loader style-loader -D
复制代码

css-loader 只负责解析 css 文件,一般须要配合 style-loader 将解析的内容插入到页面,让样式生效。顺序是先解析后插入,因此 css-loader 放在最右边,第一个先执行。

rules: [
  {
    test: /\.css$/,
    use: ['style-loader', 'css-loader']
  }
]
复制代码

但要注意最终打包出来的并非css文件,而是js。它是经过建立 style 标签去插入样式。

分离css

通过上面的 css 解析,打包出来的样式会混在 js 中。某些场景下,咱们但愿把 css 单独打包出来,这时可使用 mini-css-extract-plugin 分离 css。

安装 mini-css-extract-plugin:

npm i mini-css-extract-plugin -D
复制代码

配置 plugins:

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

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash].css',
      chunkFilename: 'css/[id].[hash].css'
    })
  ]
}
复制代码

配置 loader:

rules: [
  {
    test: /\.css$/,
    use: [
      // 插入到页面中
      'style-loader',
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // 为外部资源(如图像,文件等)指定自定义公共路径
          publicPath: '../',
        }
      },
      'css-loader',
    ]
  },
]
复制代码

通过上面配置后,打包后 css 就会被分离出来。

但要注意若是 css 文件不是很大的话,分离出来效果可能会拔苗助长,由于这样会多一次文件请求,通常来讲单个 css 文件超过 200kb 再考虑分离。

css浏览器兼容前缀

安装相关依赖:

npm i postcss postcss-loader autoprefixer -D
复制代码

项目根目录下新建 postcss.config.js:

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

package.json 新增 browserslist 配置:

{
  "browserslist": [
    "defaults",
    "not ie < 11",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]
}
复制代码

最后配置 postcss-loader

rules: [
  {
    test: /\.css$/,
    use: [
      'style-loader', 
      'css-loader',
      'postcss-loader'
     ]
  }
]
复制代码

处理先后对比:

压缩 css

webpack 内部默认只对 js 文件进行压缩。css 的压缩可使用 optimize-css-assets-webpack-plugin 来完成。

安装 optimize-css-assets-webpack-plugin:

npm i optimize-css-assets-webpack-plugin -D
复制代码

配置 plugins:

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  plugins: [
    new OptimizeCssAssetsWebpackPlugin()
  ]
}
复制代码

压缩结果:

解析图片

项目中确定少不了图片,对于图片资源,webpack 也有相应的 url-loader 来解析。url-loader 除了解析图片外,还能够将比较小的图片能够转为base64,减小线上对图片的请求。

安装 url-loader:

npm i url-loader -D
复制代码

配置 loader:

rules: [
  {
    test: /\.(jpg|png|jpeg|gif)$/,
      use: [{
        loader: 'url-loader',
        // 小于50k的图片转为base64,超出的打包到images文件夹
        options: {
          limit: 1024 * 50,
          outputPath: './images/',
          pulbicPath: './images/'
        }
    }]
  }
]
复制代码

配置完成后,只须要在入口文件内引入图片使用,webpack 就能够帮助咱们把图片打包出来了。

但有时候,图片连接是直接写到 html 中,这种状况 url-loader 没法解析。不慌,使用 html-loader 能完成这项需求。

rules: [
  {
    test: /\.(html)$/,
    use: [{
      loader: 'html-loader',
      options: {
        // 压缩html模板空格
        minimize: true,
        attributes: {
          // 配置须要解析的属性和标签
          list: [{
            tag: 'img',
            attribute: 'src',
            type: 'src',
          }]
        },
      }
    }]
  },
]
复制代码

注意这里 html-loader 只是起到解析的做用,须要配合 url-loader 或者 file-loader 去使用。也就是说解析模板的图片连接后,仍是会走上面所配置的 url-loader 的流程。

还有一点,使用 html-loader 后, html-webpack-plugin 在 html 中的插值会失效。

其余类型资源解析

解析其余资源和上面差很少,不过这里用到的是 file-loaderfile-loaderurl-loader 主要是将文件上的 import / require() 引入的资源解析为url,并将该资源发送到输出目录,区别在于 url-loader 能将资源转为 base64。

安装 file-loader:

npm i file-loader -D
复制代码

配置 loader:

rules: [
  {
    test:/\.(mp3)$/,
    use: [{
      loader: 'file-loader',
      options: {
        outputPath: './music/',
        pulbicPath: './music/'
      }
    }]
  },
  {
    test:/\.(mp4)$/,
    use: [{
      loader: 'file-loader',
      options: {
        outputPath: './video/',
        pulbicPath: './video/'
      }
    }]
  }
]
复制代码

上面只是列举了部分,须要解析其余类型资源,参照上面的格式添加配置。

解析 html 中的其余类型资源也和上面同理,使用 html-loader 配置对象的标签和属性便可。

devServer 提升开发效率

每次想运行项目时,都须要 build 完再去预览,这样的开发效率很低。

官方为此提供了插件 webpack-dev-server,它能够本地开启一个服务器,经过访问本地服务器来预览项目,当项目文件发生变化时会热更新,无需再去手动刷新,以此提升开发效率。

安装 webpack-dev-server

npm i webpack-dev-server -D
复制代码

配置 webpack.config.js

const path = require('path')

module.exports = {
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    // 默认 8080
    port: 8000,
    compress: true,
    open: true,
  }
}
复制代码

webpack-dev-server 用法和其余插件不一样,它能够不添加到 plugins,只需将配置添加到 devServer 属性下便可。

添加启动命令 package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve"
  },
}
复制代码

命令行运行 npm run dev,就能够感觉到飞通常的体验。

复制文件到 dist

对于一些不须要通过解析的文件,在打包后也想将它放到 dist 中,可使用 copy-webpack-plugin

安装 copy-webpack-plugin:

npm i copy-webpack-plugin -D
复制代码

配置 plugins:

const CopyPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          // 资源路径
          from: 'src/json',
          // 目标文件夹
          to: 'json',
        }
      ]
    }),
  ]
}
复制代码

打包前清除旧 dist

打包后文件通常都会带有哈希值,它是根据文件的内容来生成的。因为名称不一样,可能会致使 dist 残留有上一次打包的文件,若是每次都手动去清除显得不那么智能。利用 clean-webpack-plugin 能够帮助咱们将上一次的 dist 清除,保证无冗余文件。

安装 clean-webpack-plugin

npm i clean-webpack-plugin -D
复制代码

配置 plugins:

const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = {
  plugins: [
    new CleanWebpackPlugin()
  ]
}
复制代码

插件会默认清除 output.path 的文件夹。

自定义压缩选项

webpack 从 v4.26.0 开始内置的压缩插件变为 terser-webpack-plugin。若是没有其余需求,自定义压缩插件也尽可能保持与官方的一致。

安装 terser-webpack-plugin:

npm i terser-webpack-plugin -D
复制代码

配置 plugins:

const TerserPlugin = reuqire('terser-webpack-plugin')

module.exports = {
  optimization: {
    // 默认为 true
    minimize: true,
    minimizer: [
        new TerserPlugin()
    ]
  },
}
复制代码

插件压缩配置能够查阅 terser-webpack-plugin 文档,在调用时自定义选项。

添加到 minimizer 和 plugins 的区别

像上面的 css 压缩插件也能够添加到 optimization.minimizer。与配置到 plugins 的区别是,配置到 plugins 的插件在任何状况都会去执行,而配置到 minimizer 中,只有在 minimize 属性开启时才会工做。

这样作的目的便于经过 minimize 属性来统一控制压缩。

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = reuqire('terser-webpack-plugin')

module.exports = {
  optimization: {
    // 默认为 true
    minimize: true,
    minimizer: [
        new TerserPlugin(),
        new OptimizeCssAssetsWebpackPlugin()
    ]
  },
}
复制代码

注意若是你提供 minimizer 选项而没有使用 js 压缩插件,即便 webpack 内置 js 压缩,打包出来的 js 也不会被压缩。由于 webpack 压缩配置会被 minimizer 覆盖。

排查错误的建议

在使用 webpack 的过程当中,这玩意偶尔会有些奇奇怪怪的报错。

下面是我遇到的一些错误以及解决方法(仅供参考并非万能法则):

  1. 一些 loader 和 plugin 在使用时,会依赖 webpack 的版本。若是使用过程发生错误,检查是否有版本不兼容的问题,能够尝试降一个版本。
  2. 从新安装依赖,有可能下载过程当中,一些依赖会没装上。
  3. 查看使用文档,不一样版本所传入的选项属性可能会不同(被坑过) 。

还有注意控制台的提示,通常根据错误提示都能猜出大概是什么问题。

依赖版本和完整配置

项目结构:

依赖版本:

{
    "@babel/core": "^7.12.3",
    "@babel/preset-env": "^7.12.1",
    "autoprefixer": "^10.0.1",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^6.3.0",
    "cross-env": "^7.0.2",
    "css-loader": "^5.0.0",
    "file-loader": "^6.2.0",
    "html-loader": "^1.3.2",
    "html-webpack-plugin": "^4.5.0",
    "mini-css-extract-plugin": "^0.9.0",
    "postcss": "^8.1.6",
    "postcss-loader": "^4.0.4",
    "style-loader": "^2.0.0",
    "url-loader": "^4.1.1",
    "webpack": "^4.44.2",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
}
复制代码

webpack.config.js:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

/** @type {import('webpack').Configuration} */
module.exports = {
  devtool: process.env.NODE_ENV === 'development' ? 'source-map' : 'none',
  entry: {
    main: './src/js/main.js'
  },
  output: {
    filename: '[name].[hash].js',
    path: path.join(__dirname, 'dist')
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 8000,
    compress: true,
    open: true,
  },
  plugins: [
    // 清除上一次的打包内容
    new CleanWebpackPlugin(),
    // 复制文件到dist
    new CopyPlugin({
      patterns: [
        {
          from: 'src/music',
          to: 'music',
        }
      ]
    }),
    // 使用html模版
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: true
    }),
    // 分离css
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash].css',
      chunkFilename: 'css/[id].[hash].css'
    }),
    // 压缩css
    new OptimizeCssAssetsWebpackPlugin()
  ],
  module: {
    rules: [
      // 解析js(es6转es5)
      {
        test: /\.js$/,
        exclude: /node_modules/, 
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../',
            }
          },
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.(html)$/,
        use: [{
          // 主要为了解析html中的img图片路径 须要配合url-loader或file-loader使用
          loader: 'html-loader',
          options: {
            attributes: {
              list: [{
                tag: 'img',
                attribute: 'src',
                type: 'src',
              },{
                tag: 'source',
                attribute: 'src',
                type: 'src',
              }]
            },
            minimize: true
          }
        }]
      },
      // 解析图片
      {
        test: /\.(jpg|png|gif|svg)$/,
        use: [{
          loader: 'url-loader',
          // 小于50k的图片转为base64,超出的打包到images文件夹
          options: {
            limit: 1024 * 50,
            outputPath: './images/',
            pulbicPath: './images/'
          }
        }]
      },
      // 解析其余类型文件(如字体)
      {
        test: /\.(eot|ttf)$/,
        use: ['file-loader']
      },
      {
        test: /\.(mp3)$/,
        use: [{
          loader: 'file-loader',
          options: {
            outputPath: './music/',
            pulbicPath: './music/'
          }
        }]
      }
    ]
  },
}
复制代码

最后

因为单页项目简单,配置项比较朴实无华,本文主要是些基础配置。不过套路都差很少,根据项目的需求去选择 loader 和 plugin。更多的仍是要了解这些插件的做用和使用方法,以及其余经常使用的插件。

相关文章
相关标签/搜索