webpack

127 阅读7分钟

Webpack 入门-了解Webpack的基础配置

webpack 版本: 4.41.0

生成webpack 项目

新增一个目录myapp,然后进入到目录中,执行下面操作

npm init --y
npm install webpack webpack-cli --save-dev

在根目录中创建src目录,src目录下新增index.js文件; 在根目录下新建webpack.config.js文件,代码如下:

module.exports = {
  mode: "development",
  entry: {
    "main": "./src/index.js",
  },
}

在package.json 中新增script

  "scripts": {
    "build": "npx webpack",
  },

执行 npm run build,你会发现根目录中新增了一个dist目录,dist目录下有一个main.js。说明你成功用Webpack打包好项目了,接下来带你们去体会Webpack的不同配置带来的效果。

loader

loader是什么

Webpack的本质上是一个模块打包工具,默认只能打包JS文件,不能处理其他文件。在项目中我们还需要对css、图片进行打包,为了能够让webpack对其他类型的文件进行打包,在打包之前我们就必须将其他类型文件转换成Webpack能够识别的模块。 用于将其他类型文件转换成Webpack能够识别处理的工具我们就称之为 loader

特点

1、单一原则,一个loader只做一件事情 2、多个loader会按照从右到左,从下到上的顺序执行 例如: 从右到左

["style-loader", "css-loader"]
先执行css-loader解析css文件拿到所有的内容呢,
再执行style-loader将内容插入到HTML的HEAD中

file-loader

index.js 中引入图片

import "./images/abc.jpg"

打包后,控制台打印出错误“You may need an appropriate loader to handle this file type” 所以我们要添加 file-loader 去打包图片

npm install file-loader --save-dev

webpack.config.js 中配置file-loader

      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
          }
        ]
      }

默认情况下 fileloader生成的图片名称就是文件内容的 MD5 哈希值 如果想打包后不修改图片的名称,可以新增配置 name: "[name].[ext]"

默认情况下 fileloader 生成的图片会放在dist 根目录下,如果想要放在你指定的目录下,可以配置 outputPath: "images" 如果你的图片资源会被托管在其他cdn服务器上,可以新增配置 publicPath: "托管服务器地址"

打包iconfont

字体图标也是文件,所以我们用file-loader来处理字体图标文件

{
    test: /\.(eot|svg|ttf|woff|woff2)$/,
    use:[{
        loader: "file-loader",
        options: {
            name: "[name].[ext]",
            outputPath: "font/",
        }
    }]
}

url-loader

url-loader, url-loader的功能类似于 file-loader 如果你的图片资源体积比较小,可以打包成base64的字符串

使用: 1、npm install url-loader --save-dev 2、

      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name].[ext]",
              outputPath: "images",
              /**
                limt: 指定图片限制的大小
                如果被打包的图片大于 limt,就会将图片打包成一个文件;
                如果被打包的图片小于 limt,就会将图片打包成 base64的字符串
               */
              limit: 1024,
            }
          }
        ]
      }

css-loader

css-loader: 解析css文件的@import依赖关系 style-loader: 将webpack处理之后的内容插入到HTML的HEAD代码中 npm install style-loader css-loader

{
  test: /\.css$/,
  use: ["style-loader", "css-loader"]
}

css-loader 模块化

默认情况下 import "./index.css" 导入的样式是全局样式;也就是只要被导入,在其他文件也可以使用;如果想要导入的css文件只在导入的文件中有效,那么就需要开启css模块化

{
    loader: "css-loader",
    options: {
        modules: true // 开启CSS模块化
    }
}

使用: import style from "./index.css" 然后在使用的地方通过 style.classname 方式使用即可

原理: 原来的样式:

.box {
  width: 100px;
}

生成的样式

._2cQdM_4_hvjjigyhxT_FtA {
  width: 100px;
}

你会发现css的类名变了,不是原来的类名。开启模块化后,会将类名修改为全局唯一的类名,这就使得样式只能在导入的文件中有效

less-loader

自动将less转换成css 使用: 1、npm install less less-loader --save-dev 2、

{
  test: /\.css$/,
  use: ["style-loader", "css-loader", "less-loader"]
}

scss-loader

自动将scss转换成css 使用 1、npm install node-sass sass-loader 2、

{
  test: /\.css$/,
  use: ["style-loader", "css-loader", "sass-loader"]
}

postcss-loader

postcss和sass/less不同,它不是css 预处理器。它是一款使用插件去抓换css的工具。postcss有许多非常好用的插件。 例如: autoprefixed(自动补全浏览器前缀) 通常W3C组织成员提出一个新属性,比如:transform、transition,大家都觉得好,但是W3C制定标准,要走很复杂的程序。而浏览器商推广时间紧,如果一个属性已经够成熟了,就会在浏览器中支持。为了必满日后W3C公布标准时有所更改,加了一个私有前缀。比如:-webkit-transform,通过中这种方式来提前支持新属性。

postcss-pxtorem(自动把px转换成rem) 使用: 1、npm install postcss-loader autoprefixer --save-dev 2、在css-loader、less-loader、sass-loader后面加上postcss-loader

{
  test: /\.css$/,
  use: ["style-loader", "css-loader", "less-loader", "postcss-loader"]
}

3、新增postcss.config.js

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                // "ie >= 8", // 兼容IE7以上浏览器
                // "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                // "chrome  >= 35", // 兼容谷歌版本号大于35浏览器,
                // "opera >= 11.5" // 兼容欧朋版本号大于11.5浏览器,
                "chrome  >= 36", // 如果需要适配的浏览器完全兼容则不会添加前缀
            ]
        }
    }
};

webpack-dev-server

  • 概念

webpack-dev-server 可以将我们打包好的程序运行在一个服务器环境下。可以监听文件的变化,也可以解决企业开发阶段的跨域问题。

  • 使用
npm install webpack-dev-server --save-dev

webpack.config.js中配置webpack-dev-server

  devServer: {
    port: 3000
  }

package.json中增加命令

  "scripts": {
    "watch": "npx webpack-dev-server --config webpack.config.js"
  },

执行 npm run watch 然后在浏览器中打开http://localhost:3000/,在入口文件index.js增加一行代码

console.log("监听文件的变化")

保存后你会发现浏览器自动重新刷新了,控制台打印了 “监听文件的变化”

热更新(HMR)

  • 概念

通过webpack-dev-server 可以实现实时监听打包内容的变化。但是每次打包后都会重新刷新网页,给我们带来了很多的不便。而HMR 会在内容发生改变的时候更新内容但是不会重新刷新网页。

  • 缺点 比如说我的入口文件代码如下:
let btn = document.createElement("button");
btn.innerText = "添加内容";
document.body.appendChild(btn);

let index = 0;
btn.onclick = function () {
    let p = document.createElement("p");
    p.innerText = "我是第" + index + "个段落";
    index++;
    document.body.appendChild(p);
};

先点击按钮,界面上会出现一行文字:“我是第0个段落”,然后我此时去修改入口文件的代码,此时界面重新刷新了,界面上的一行文字也消失了。但是在实际的开发中,我们不希望重新刷新,希望能够更新修改的内容,这时就用到了HMR。

  • 使用

webpack.config.js中配置

  devServer: {
    port: 3000,
    hot: true
  },

入口文件index.js中增加下列代码

if (process.env.NODE_ENV === 'development') {
  const HMR = module.hot;
  HMR && HMR.accept && HMR.accept();
}

重新执行 npm run watch,重复上面的操作呢,你会发现界面上的“我是第0个段落”还显示,修改的部分也自动更新了。

output

告诉webpack在哪里放置它所创建得bundle,以及如何命名这些文件。默认的输出目录是 "./dist",默认入口文件的名称为 "main.js"

重点讲解下 publicpath(项目中经常用到)

  output: {
    path: path.join(__dirname, '/dist'),
    filename: "js/main.js",
    publicPath: "http://localhost:3000/rp/static"
  },

多入口文件

webpack.config.js 配置多个入口

  entry: {
    "main": "./src/js/main.js",
    "home": "./src/js/home.js"
  },

配置多个出口

  plugins: [new HtmlWebpackPlugin({
    template: "./src/index.html",
    filename: "index.html",
    chunks: ["main"]
  }), new HtmlWebpackPlugin({
    template: "./src/test.html",
    filename: "test.html",
    chunks: ["home"]
  })]

执行npm run build,你会发现dist目录下出现index.html和test.html。index.html中会自动引入 main.js;test.html中会自动引入 home.js; 图片转存失败,建议将图片保存下来直接上传转存失败,建议直接上传图片文件

Dll 构建

就是把打包一些不会经常改变、常用、构建时间长的第三方包提前打包好,然后webpack打包的时候跳过这些包,从而达到提高打包效率的效果; 1、单独配置一个config.js打包不会发生改变的第三方库,并生成一个映射文件 manifest.json(表明打包了哪些第三方库)

module.exports = {
    mode: 'production',
    entry: {
        vendors: 'jquery'
    },
    output: {
        filename: '[name].[contenthash:8].js',
        path: path.resolve(__dirname, 'dll'),
        library: '[name]' // 表示打包的是一个库, 表示将打包的内容通过全局变量暴露出去
    },
    plugins: [
      new Webpack.DllPlugin({
        name: '[name]',
        path: path.resolve(__dirname, 'dll/[name].manifest.json')
        // 通常我们会把好几个库打包到一个文件中,所以我们需要一个映射文件方便webpack能从中找到其中的库
      })
    ]
};

2、告诉webpack 对应的映射文件,在打包的时候如何webpack回到指定的映射文件中查找对应的动态库, 找到了那么就不会重新打包动态库中的内容了, 如果找不到才会重新打包

  new Webpack.DllReferencePlugin({
    manifest: path.resolve(__dirname, "dll/venders.manifest.json")
  }),

3、将打包好的库引入到界面上 npm install add-asset-html-webpack --save-dev

  new AddAssetHtmlWebpackPlugin({
    filepath: path.resolve(__dirname,'dll/venders.dll.js')
  })

动态链接库虽然能够减少构建的时间,但是这个效果是非常微弱的。在项目我们考虑使用dll 和 HardSourceWebpackPlugin一起使用去大幅度的减少构建的时间。

构建加速

HardSourceWebpackPlugin

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  // ......
  plugins: [
    new HardSourceWebpackPlugin() // <- 直接加入这行代码就行
  ]
}

加这个插件之前项目构建的速度是

Version: webpack 4.28.4
Time: 49739ms

加了这个插件之后项目构建的速度为

Version: webpack 4.28.4
Time: 148730ms

直接缩短了 100000ms 构建速度大大提升了。

如果是webpack5的话,已经这个插件已经加入了webpack5。

externals

以 script的方式引入不会经常变化的第三方库,然后告诉Webpack打包的时候遇到这些第三方库 不需要打包进来我们的项目,从而提升打包速度。

1、html文件中引入 jquery

html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Webpack Project</title>
  </head>
  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
  <body>
    <div id="root"></div>
    <!--这是test-->
  </body>
</html>

2、配置告诉webpack 不去打包 jquery,当遇到使用第三方库jquery 就去使用全局变量 jQuery

  externals: {
    jquery: "jQuery"
  },

3、入口文件引用 jquery

import $ from "jquery"

const divEle = $("<div></div>")
divEle.text("你好呀")
$("body").append(divEle)

npm run build后 你会发现打包的文件中没有 打包进query相关的代码; npm run watch后,你会发现界面上显示 "你好呀"

代码分割

什么是代码分割

webpack打包会默认将加载的所有模块打包到一个文件中。比如说a.js引入了b.js, a.js有1kb,b.js也有1kb;当我们修改了a.js时,但没有修改b.js时,用户需要重新去加载2kb的文件; 解决方案:将不常改变的模块打包成另一个文件;这样每次修改后用户只需要加载修改后的文件即可。

生产环境

生成环境中默认情况下对 异步加载的模块进行代码分割

异步加载模块是什么呢?

同步加载:import $ from 'jquery'
在index.js 中导入了10个模块,那么只要index.js执行,就会一次性将10个模块加载进来
异步加载:import("jquery").then(({default: $}) => { 使用模块代码})
在index.js中导入了10个模块,那么哪怕index.js执行,也要看是否满足条件才去加载。

我们理解了异步加载模块后,开始按下面的步骤操作:

// index.js(入口文件)
const ele = document.createElement("div")
ele.innerText = "你好呀"
ele.onClick = () => {
  import("jquery").then(({default: $}) => {
    console.log("default", $)
  })
}

//webpack.config.js
mode: "production"

npm run build 后你会发现打包后生成的js文件有两个,一个是入口文件,另一个就是jquery文件,jquery文件被分割出来一个单独的文件了。

如果你想要修改代码分割出来的文件的名称,可以使用魔法注释/* webpackChunkName: "jquery" */

  import(/* webpackChunkName: "jquery" */"jquery").then(({default: $}) => {
    console.log("default", $)
  })

如果想要空闲时间去加载模块,而不是等满足条件后去加载模块,可以使用/* webpackPrefetch: true */:

 import(/* webpackPrefetch: true */"jquery").then(({default: $}) => {
    console.log("default", $)
  })

生产环境代码分割默认情况下的配置

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

开发环境

开发环境不会自动对异步加载模块进行代码分割;需要的话,要在webpack.config.js 中配置

// webpack.config.js
optimization: {
  splitChunks: {
    chunks: "async",
  }
},

tree-shaking

什么是 tree-shaking

过滤掉无用的JS代码和css代码,我们称之为 tree-shaking 例如:b.js暴露出两个方法,a.js中引入b.js, 并且导入了其中一个方法并使用。默认情况下会把b模块中的两个方法都会打包到a.js文件中。

为了提高性能,我们希望只把用到的那一个方法打包到a.js文件中。

生产环境

生产环境默认会自动tree-shaking

// a.js
import {fun1} from "./main"

fun1()

// b.js
export const fun1 = function() {
  console.log("fun1")
}

export const fun2 = function() {
  console.log("fun2")
}

打包后的 a.js不包括 fun2的代码 注意:只有ES 模块才会支持 Tree-shaking

import "./css/index.css"
import  "./less/index.less"
import "./scss/index.scss"

生产环境下也会默认不会对 css、less、scss文件进行摇树

开发环境

开发环境默认情况下不会进行 tree-shaking; 如果要tree-shaking,要在webpack.config.js中配置

// webpack.config.js
  optimization: {
    usedExports: true,
  },
// package.json
sideEffects: ["*.css", "*.less", "*.scss"] // 告诉Webpack 哪些文件不做tree-shaking

打包后的a.js文件中依然有fun2的代码,但是有个魔法注释告诉webpack这是没有用到的代码

/* unused harmony export fun2 */

libraryTarget和library

当用Webpack去构建一个可以被其他模块导入使用的库时,需要用到LibraryTarget和library.

image.png

当不设置libraryTarget的值时,默认为 var

output: {
library: "MyLib"
}

此时打包后会将入口起点的返回值分配给一个变量。

var MyLibrary = _entry_return_;

// 在一个单独的 script...
MyLibrary.doSomething();

在window环境中,全局变量会默认分配给window,由此我们可以这样使用打包之后的文件。

  <script src="./dist/main.js"></script>
  <script>
    window.MyLib.default();
  </script>

当设置 libararyTarget为 umd的时候

要兼容node环境和浏览器环境,需要额外设置 globalObject属性

    output:{ 
       library: "MyLib",
        libraryTarget: "umd",
        globalObject: "this"
    }
    
    为了使 UMD 构建在浏览器和 Node.js 上均可用,应将 `output.globalObject` 选项设置为 `'this'`。对于类似 web 的目标,默认为 `self`。

这样才能在node环境上使用

const MyLib = require("../dist/main.js")
console.log("MyLib", MyLib.default())

当需要通过ES导入时,还需要配置experiments.outputModule

  experiments: {
    outputModule: true,
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    library: {
      type: "module",
    },
  },

创建一个库

入口文件index.js,导出一个函数

console.log("Hello World!");

export function getName() {
  return "jjj";
}

打包配置文件使用output.library配置项暴露从入口起点导出的内容

 entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    library: "webpackNumbers",
  },

我们将入口暴露为 webpackNumbers,这样用户就可以通过脚本标签使用它。

  <script src="./dist/main.js"></script>
  <script>
    console.log(window.webpackNumbers.getName());
  </script>

然而它只能通过被脚本标签引用而发挥作用,而不能运行在CommonJS、AMD、NodeJS等环境中。 作为一个库作者,我们希望它能兼容不同的环境。 接下来更新ouput.library配置项,将其type设置为“umd”

    library: {
      type: "umd",
      name: "webpackNumbers",
      globalObject: "this"
    },

这样就可以在CommonJS、AMD中加载文件。