不懂Webpack4的前端不是好工程师(基础篇)

413 阅读12分钟

webpack 究竟是什么?

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

传统模式开发

新建 index.html,如下:

<!DOCTYPE html>
<html lang="en">
<body>
  <p>这是网页内容</p>
  <div id="root"></div>
  <script src="./header.js"></script>
  <script src="./content.js"></script>
  <script src="./footer.js"></script>
  <script src="./index.js"></script>
</body>
</html>

再建立index.jsheader.jscontent.jsfooter.js

//index.js
new Header()
new Content()
new Footer()
//header.js
function Header () {
  var dom = document.getElementById('root')
  var header = document.createElement('div')
  header.innerText = 'header'
  dom.append(header)
}
//content.js
function Content () {
  var dom = document.getElementById('root')
  var content = document.createElement('div')
  content.innerText = 'content'
  dom.append(content)
}
//footer.js
function Footer () {
  var dom = document.getElementById('root')
  var footer = document.createElement('div')
  footer.innerText = 'footer'
  do
m.append(footer)
}

这样面向对象 new 出各个模块的构造函数,在一定程度上比面相过程全部写在一个文件上好维护,但是HeaderContet这些构造函数指向问题还要回到index.html文件一一对应查找,就造成了在index.js上指定不明确,如果我们能像以下代码一样导入就能解决这个问题了

//index.js
import Header from './header.js'
import Content from './content.js'
import Footer from './footer.js'

new Header() new Content() new Footer()

遗憾的是,浏览器不会识别这种 es6 的语法,使用浏览器访问index.html会发现控制台会报错。此时我们的webpack就可以为了解决这种问题而诞生了

使用 webpack 模块化开发

首先想使用webpack,我们得先npm初始化包并安装 webpack

npm init
npm install webpack webpack-cli -D

想用ESmodule 语法导出,我们得修增加导出语法

//header.js
export default Header
//content.js
export default Content
//footer.js
export default Footer

然后我们就可以使用npx 运行 webpack 执行 index.js 文件

npx webpack index.js

执行后,我们发发现根目录下生成了一个 dist/main.js目录文件,这便是 webpack 默认出口输出(output),后面会介绍。这个文件便是 webpack 翻译我们的index.js生成后的文件,所以有的人会称webpackJS的翻译器,但这说法也并不完成正确。既然我们翻译了文件,我们就需要在index.html修改成导入后的文件,如下修改:

//index.html
<script src="./dist/main.js"></script>

修改完成后打开浏览器重新访问index.html发现可以生成相对于模块并且没有报错了,这就是webpack的模块开发。

使用 webpack 的配置文件(核心概念)

webpack 的核心概念

  • entry: 入口

  • output: 输出

  • loader: 模块转换器,用于把模块原内容按照需求转换成新内容

  • 插件(plugins): 扩展插件,在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情

使用一个配置文件

webpack v4 中,可以无须任何配置,因为webpack提供了默认配置,然而大多数项目会需要很复杂的设置,这就是为什么 webpack 仍然要支持 配置文件。这比在 terminal(终端) 中手动输入大量命令要高效的多,所以让我们在根目录创建一个配置文件:webpack.config.js(默认配置文件,可通过npx webpack --config webpack.config.js修改)

//webpack.config.js
const path = require('path')
module.exports = {
  entry: './src/index.jss' , //入口文件 默认:src/index.js
  output: { //出口文件 默认: dist/main.js
    filename: 'bundle.js', //输出的文件名
    path: path.resolve(__dirname,'dist') //输出的路径,只能是绝对路径
  }
}

新建 src 文件夹,移动index.js,header.js等 js 到 src 文件夹,执行以下代码会发现 dist 文件夹生成了新的出口文件bundele.js

npx webpack src/index.js

entry

配置需要打包入口文件

//webpack.config.js
<!--单个文件-->
module.exports = {
  entry: './src/index.jss' , //入口文件 默认:src/index.js
}

<!--打包多个入口文件--> module.exports = { entry: { main: './src/index.js', //入口文件 默认:src/index.js sub: './src/sub.js' }, }

output

配置打包输出的文件

  output: { //出口文件 默认: dist/main.js
    filename: 'bundle.js', //输出的文件名
    path: path.resolve(__dirname,'dist') //输出的路径,只能是绝对路径
  }

<!--多个入口文件需要不同名称文件输出配置--> output: { //出口文件 默认: dist/main.js filename: '[name].js', //输出的文件名 path: path.resolve(__dirname, 'dist') //输出的路径,只能是绝对路径 },

npm scripts

考虑到用 CLI 这种方式来运行本地的 webpack 副本并不是特别方便,我们可以设置一个快捷方式。调整 package.json 文件,添加在 npm scripts 中添加一个 npm 命令:

//package.json
"scripts": {
    "bundle" : "webpack"
},

现在,可以使用 npm run build 命令,来替代我们之前使用的 npx 命令。注意,使用 npm scripts,我们可以像使用 npx 那样通过模块名引用本地安装的 npm packages。这是大多数基于 npm 的项目遵循的标准,因为它允许所有贡献者使用同一组通用脚本(如果必要,每个 flag 都带有 --config 标志)。

现在运行以下命令,然后看看你的脚本别名是否正常运行:

npm run bundle

浅析 webpack 打包输出内容

npm run bundle
> webpack4@1.0.0 bundle E:\project\webpack4
> webpack
Hash: 768c04b37ed214487576
Version: webpack 4.42.0
Time: 98ms
Built at: 2020-03-24 4:57:54 PM
    Asset      Size  Chunks             Chunk Names
bundle.js  1.29 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js + 3 modules 741 bytes {0} [built]
    | ./src/index.js 147 bytes [built]
    | ./src/header.js 202 bytes [built]
    | ./src/content.js 198 bytes [built]
    | ./src/footer.js 194 bytes [built]

WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: webpack.js.org/configurati…

webpack打包输出的结果我们可以看出有个警告,那是因为我们没有指定打包输出的环境('development' or'production'),我们可以在webpack.config.js新增以下代码:

module.exports = {
  mode: 'development', //默认为production
  ...
}

重新执行npm run bundle 发现不会提出警告了,并且生成的bundle.js的代码没有被压缩

什么是 loader

webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。如果你在默认配置下打包除 js 文件外出错,所以我们要借助loader来打包 js 外的文件

现在我们在src文件下存放一张logo.jpg的图片,然后在 index.js 引入后使用webpack打开

//index.js
const logo = require('./logo.jpg')

执行npm run bundle打包后会抛出如下错误:

ERROR in ./src/logo.jpg 1:0
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)
 @ ./src/index.js 5:13-34

这是因为 webpack 像我们上面说的一样,只能打包 js 文件,如果想打包除 js 以外的静态资源,此时我们就需要借用loader来帮助我们打包图片资源。想打包图片我们首先得先安装相关的loader

file-loader

npm install file-loader -D

然后我们需要在webpack.config.js文件下配置相关的module规则:

//webpack.config.js
...
module: {
    rules: [
      {
        test: /\.jpg$/,
        use: [
          {
            loader: 'file-loader' //使用相对应的loader
          }
        ]
      }
    ]
  }

控制台npm run bundle再次运行时,发现打包成功没有报错,并且在 dist 出口目录生成了相应的图片资源。那我们导入的图片资源变量会是什么呢,我们试着打印一下:

const logo = require('./logo.jpg')
console.log('logo',logo)

可以在浏览器中看出,我们获取到的是打包生成的文件相关信息,包括图片名字。经过这个例子,我们就明白了在vue的脚手架项目中可以这样引入.vue 相关文件的了

import Header from 'Header.vue'
//webpack.config.js
rules: [
    ...
      {
        test: /\.vue$/,
        use: { //只使用一个loader可以直接用对象配置
          loader: 'vue-loader'
        }
      }
    ]

上面案例中,我们可以看到options字段,这是我们可以打包文件资源的时候定义相对应的规则,比如:

rules: [
      {
        test: /\.jpg$/,
        use: [
          {
            loader: 'file-loader', //使用相对应的loader
            options: {
              name:'[name].[ext]', //定义打包生成的文件名字
              outputPath: 'images' //定义输出的文件目录:dist/images 下
            },
          }
        ]
      }
    ]

这样我们就可以自定义打包文件的名字和目录,更多的规则可以查看官方文档配置

url-loader

我们可以用url-loader取代file-loader来实现静态文件资源打包,那为什么我们有 file-loader 还要用 url-loader,让我看看下面的例子就知道了:

首先我们先npm install url-loader --save-dev安装file-loader,然后在webpack.config.js进行相对应的配置

rules: [
      {
        test: /\.jpg$/,
        use: [
          {
            loader: 'url-loader', //使用相对应的loader
            options: {
              limit: 10240 //单位:字节 超过10kb 的文件生成图片,否则生成base64编码
            }
          }
        ]
      }
    ]

运行打包后我们可以发现,超过 10kb 的文件生成图片,否则生成base64编码。这样做的好处是什么呢,图片生成 base64 编码可以大大减少我们 https 请求,但不是所有的图片都生成 base64 编码,比如图片几 M 的生成的话,相对应 js 文件大小也会增加,网页打空白的时间也相对应增加,至于哪些需要转,limit 需要设置多大限制根据自己的项目来,我的项目中一般是 icon 图标类的会转 base64 编码,其他大的相对应打包生成文件。

使用 loader 打包样式静态资源

当我们尝试打包 css 文件的时候,如果没有使用相对应的样式loader就会打包失败。我们在src目录下创建新的文件index.csslogo.css,并且在 index.js 引入该样式文件:

//logo.css
.logo{
  width: 100px;
  height: 100px;
}
//index.css
@import './logo.css'
//index.js
import logo from './logo.jpg'
import './index.css'

var img = new Image() img.src = logo img.classList.add('logo') var root = document.getElementById('root') root.append(img)

打包之后可以看到控制台抛出了相对应的报错,此时我们应该接入css-loaderstyle-loader来解决这个问题,首先我们先得安装两个loader

npm install css-loader style-loader -D
   rules: [
    ...
      {
        test: /\.css$/,
        //注:打包执行顺利从右到左,从下到上,不能颠倒,先接css-loader转换语法在使用style-loader解析到头部标签
        use: ['style-loader','css-loader']
      }
    ]

css-loader

主要用于打包 css 文件中解析@import等语法,将 CSS 转化成 CommonJS 模块。

css-loader还可以配置更多的选项,比如importLoadersmodules等。如果没有配置importLoaders,在一个 scss 文件中@import其他的 scss 文件,可能该导入的 scss 文件不会生效 css-loader 后面配置的 loader(sass-loader,postcss-loader)

use: ['style-loader',{
          loader: 'css-loader',
          options: {
            importLoaders: 2 // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
          }
        },'sass-loader','postcss-loader'
    ]

配置modules参数为 true 可以模块化导入相关的样式文件,否则会全局样式污染。如下列案例:

//index.js
import logo from './logo.jpg'
import './index.scss'
import createLogo from './logo.js'

createLogo() var img = new Image() img.src = logo img.classList.add('logo') var root = document.getElementById('root') root.append(img)

//logo.js
import logo from './logo.jpg'
function createLogo () {
  var img = new Image()
  img.src = logo
  img.classList.add('logo')
  var root = document.getElementById('root')
  root.append(img)
}
export default createLogo

上面因为没有配置相关的样式模块导入,所以导入index.scss文件的样式都在两张图片成功生效,下面我们增加下模块配置引入:

    //webpack.congig.js
      ...
        test: /\.scss$/,
        use: ['style-loader', {
          loader: 'css-loader',
          options: {
            importLoaders: 2, // 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
            modules: true //按模块化引入
          }
        }, 'sass-loader', 'postcss-loader'
        ]
      }
//index.js
import logo from './logo.jpg'
import style from './index.scss'
import createLogo from './logo.js'

createLogo() var img = new Image() img.src = logo img.classList.add(style.logo) var root = document.getElementById('root') root.append(img)

重新打包后,我们发现只有 index.js 文件的图片生效了样式,我们模块化导入样式成功,更多的options配置样式可以查看官方文档

style-loader

配合css-loader使用,以形式在 html 页面中头部标签插入css代码。

sass-loader

npm install sass-loader node-sass webpack --save-dev

我们除了安装sass-lader外,并且还需要你预先安装 Node Sass。 这可以控制所有依赖的版本, 并选择要使用的 Sass 实现。新建src/index.sass

//index.sass
body {
  .logo{
    width: 100px;
    height: 100px;
  }
}
//index.js
import logo from './logo.jpg'
import './index.sass'

var img = new Image() img.src = logo img.classList.add('logo') var root = document.getElementById('root') root.append(img)

我们需要在webpack.config.js新增相对应的规则配置:

//webpack.config.js
 rules: [
    ...
      {
        test: /\.sass$/,
        use: ['style-loader','css-loader','sass-loader'] //先把sass转成css ,再进行起来的loader操作(右到左)
      }
    ]

配置后重新执行npm run bundle打包,在浏览器中可以正常访问,把sass-loader去掉再打包后,可以查看控制台头部样式中sass的语法没有转成css,这就是 sass-loader 的作用

postcss-loader

npm install postcss-loader -D

postcss-loader可以对css3样式前缀自动补全,兼容各个浏览器,使用postcss-loader前我们得配置相关的插件等,根目录下新建postcss.config.js,安装相对应的插件:autoprefixer(补全 css3 语法插件)

npm install autoprefixer -D
//postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}
//webpack.config.js
rules: [
  ...
  {
    test: /\.scss$/,
    use: ['style-loader','css-loader','sass-loader','postcss-loader']
  }
]

autoprefixer补全得结合browserslist一起使用

//package.json
"browserslist": [
    "defaults",
    "ie >= 10",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
 ],

使用 plugins 让打包更快捷

插件(plugins): 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情,就类似vue生命周期钩子一样,在某种场景,帮你做某些事情。官方已收录的插件

HtmlWebpackPlugin

一种用于打包生成 html 的插件:HtmlWebpackPlugin会在打包结束后,自动生成 html 文件,并把打包生成的 js 自动引入到这个 html 文件中。具体配置可查看HtmlWebpackPlugin 文档

//安装HtmlWebpackPlugin文档
npm install --save-dev html-webpack-plugin
//webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    ...
    plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html' //生成的模板文件
    }),

] }

//public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>html 模板</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

CleanWebpackPlugin

有时候我们打包总是要手动删除掉上一次打包的文件,我们就想有没有什么工具能帮助我们在打包前自动删除掉 dist 目录,CleanWebpackPlugin就可以帮我们解决这个问题,详细配置

//webpack.config.js
  ...
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
  ]

source-map

source-map 可以解决打包后代码报错的地方是打包后的代码而不是源业务代码的问题

//index.js
console.log('devtool',test)

这是未设置 source-map 报错的打包代码 我们在 webpack.config.js 配置下

devtool: 'source-map'

配置后重新打包久可以看到报错的是原业务代码,但是我们不建议直接用source-map,我建议开发环境使用eval-cheap-module-source-map,生成环境用cheap-module-source-map,不同的配置打包的速度不一样,可以简单总结,source-map 会生成.map 文件来映射,打包速度会很慢,因为还要映射打包文件,inline可以不生产.map 文件,直接打包在出门文件里面转成和 base64,cheap可以只报行除出错而不加列出错,module 可以让第三方 loader 插件也生效报错,eval可以直接执行 eval 函数所以速度最快,具体可以参考官方文档配置

webpackDevServer

有时候我们修改了打包入口的文件,总是要重新打包编译打开浏览器访问,有没有一种配置能让我们监听到入口文件修改,就能自动打包编译在浏览器刷新呢,webpackDevServer就可以帮你做到,webpackDevServer会在本地帮你的项目搭建一个服务器来跑,只要你更新它就可以帮你重新打包编译~

首先我们得安装 webpackDevServer

npm install webpack-dev-server -D

然后配置相关的参数

//webpack.config.js
module.exports = {
 ...
  devServer: {
    contentBase: './dist',
    port: 9000, //服务端口号
    open: true, //首次打包编译自动打开浏览器
    proxy: {//反向代理,一般用于解决跨域问题
      '/api': 'http://localhost:3000'
    }
  },

执行npm run start 就可以打包编译帮你打开相关的服务了~

//package.json
"scripts": {
    "start": "webpack-dev-server"
 },

Hot Module Replacement

有时候我们需要做的是,改了该模块的代码,浏览器不刷新,只更新该的模块代码上去,Hot Module Replacement就能帮我们实现这个效果。

//webpack.config.js
const webpack = require('webpack')
 devServer: {
    ...
    hot: true,//使用 Hot Module Replacement
    hotOnly: true, //Hot Module Replacement 出错的时候,浏览器照样不刷新
  },
  plugins: [
    ...
    new webpack.HotModuleReplacementPlugin()
  ]

js 模块代码更新的话还需要增加,css 模块的话css-loader已经帮我们处理了,那像 vue 的文件修改vue-loader也已经做了相关的处理

if (module.hot) {
  module.hot.accept('./number.js', function() {
    // Do something with the updated library module...
  });
}

babel

我们在项目中为了更好的提高开发效率会使用一些ES6语法,虽然很方便快捷, 但是ES6语法在低版本浏览器并不完全支持,这样就会导致语法报错,所以我们需要借助babel来转发成ES5来兼容各个浏览器

// index.js
import "@babel/polyfill"; //引入polyfill:可实现ES6语法解释
const p = new Promise(() => {})
let newArr = [1,2,3]
newArr.map((item) => {
  return item
})

将其安装为开发依赖项


<!--正常业务使用babel-->
npm install --save-dev babel-loader @babel/core
npm install @babel/preset-env --save-dev
<!--使用polyfill-->
npm install --save @babel/polyfill
<!--使用插件-->
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
npm install --save @babel/runtime-corejs2
module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader", //只用建于和webpack通讯,并未来解决转换问题
        options: {
          // "presets": [["@babel/preset-env",{
          //   targets: {
          //     chrome: "67"
          //   },
          //   useBuiltIns: 'usage' //可以根据业务代码只转相关ES6,大大减少打包体积
          // }]] //用于转换ES6语法成ES5识别语法,但未能解释Promise map 等语法
          // "plugins": [
          //   [
          //     "@babel/plugin-transform-runtime",
          //     {
          //       "absoluteRuntime": false,
          //       "corejs": 2, //默认flase
          //       "helpers": true,
          //       "regenerator": true,
          //       "useESModules": false,
          //       "version": "7.0.0-beta.0"
          //     }
          //   ]
          // ]
        }
      },

我们可以使用 presets 的方式,也可以使用 plugins 的方式。当我们需要做第三方库或者插件的时候,我们就需要使用 plugins 的方式,因为 presets 的方式会造成全局污染,当我们配置项太多的时候,我们也可以在根目录抽出单独的文件.babelrc来配置

//.babelrc
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 2, //默认flase
        "helpers": true,
        "regenerator": true,
        "useESModules": false,
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}

本文使用 mdnice 排版