从头学习 Webpack 的工作原理

Webpack 是什么?

根据官方文档:

"Webpack 是现代 JavaScript 应用程序的 静态模块打包工具。"

作为 JavaScript 开发人员,我们知道什么是模块,但是在 webpack 中,模块有点不同。它们包括:

  • ES Modules ------ import 声明

  • CommonJS Modules ------ require() 声明

  • AMD Modules ------ definerequire 声明

  • CSS imports ------ @import 任何 css/sass/less 文件内的语句

  • Image URLs ------ url(...)<img src="..." />

webpack 以一种可以将所有内容导入 JavaScript 代码的方式统一了所有这些不同的模块。

. . .

我们应该学习 Webpack 吗?

如今,大多数应用程序都是使用 React/Vue 或任何其他库构建的。 他们提供 CLI 工具(例如 create-react-app、@vue/cli)来创建应用程序。 这些 CLI 工具抽象了大部分配置并提供默认值。 但对于作为开发人员的我们来说,了解其工作原理是有益的,因为可能迟早需要对默认值进行一些调整。

. . .

Let's Get Started

我们将创建一个简单的应用程序来演示 webpack 的工作原理。

我们首先创建一个新目录,并初始化一个 npm 项目。 假设你位于新创建的目录中,请输入以下命令:

复制代码
npm install -y

现在安装所需的 webpack 包。

复制代码
npm install --save-dev webpack webpack-cli webpack-dev-server

打开 package.json 文件,删除已存在的 test 脚本,然后添加一个新脚本 dev ,这用于在开发环境下运行 webpack。当我们开发时,这个脚本很方便。 改完脚本之后的 文件应类似以下内容:

如果你现在运行 npm run dev,会得到一个错误:Entry module not found。这是因为,默认情况下,webpack 需要一个文件 src/index.js 作为入口点。此外,webpack 将构建的文件输出到默认调用的目录 dist 中。

让我们添加一个新文件 src/index.js 来解决这个问题。我们在文件中输入一条语句 console.log(hello)

让我们运行 npm run dev。这时没有任何问题了,同时,还会在 dist 文件目录下发现构建的 main.js

. . .

配置 webpack

要配置 webpack,我们需要在根目录中创建一个名 webpack.config.js 的文件。 在这个文件中,我们需要导出一个配置对象。 Webpack 运行在没有用户界面的 JavaScript 环境中,如 Node.js。

一些最广泛使用的术语是:

  • Enter point ------ 指定 webpack 的入口点,从中收集所有依赖项。这些依赖关系形成了依赖关系图。

  • Output ------ 指定在构建过程中生成的 JS 和静态文件的位置

  • Loaders ------ 第三方扩展,帮助 webpack 处理各种文件扩展名。它们将 JS 以外的文件转换为模块。

  • PLugins ------ 第三方扩展,可以改变 webpcak 的工作方式。

  • Mode ------ 指定两种模式,development 和 production,默认是 production。

现在让我们配置基本入口点和输出目录。

更改入口点

如果我们需要 webpack 查看 source/index.js 而不是默认的 src 文件夹,我们需要在导出的对象中添加一个属性 entry

js 复制代码
module.exports = {
    entry: './source/index.js'
}

也可以使用替换语句进行输入:

js 复制代码
const path = require('path')

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'source', 'index.js')
    }
}

更改输出目录

假设我们想要将构建的文件输出到另一个名为 build 的目录中,而不是默认的 dist 目录。 我们可以像这样设置输出属性:

js 复制代码
const path = require('path')

module.exports = {
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'main.js'
    }
}

. . .

将构建的文件包含在 HTML 文件中

在每个 Web 应用程序中,至少有一个 HTML 文件。 要使用 HTML,我们必须使用一个名为 html-webpack-plugin 的插件。让我们首先使用以下命令安装该插件:

复制代码
npm install --save-dev html-webpack-plugin

这个插件做什么?

它加载我们的 HTML 文件,并将包注入到同一个文件中。

让我们创建一个简单的 HTML 文件并在 webpack 配置文件中配置。

我将在 source 目录中创建一个名为 index.html 的文件,并在其中插入一些样板代码。 现在让我们在 webpack 文件中进行配置。

js 复制代码
const HTMLWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    plugins: [
        new HTMLWebpackPlugin({
            template: path.resolve(__dirname, 'source', 'index.html')
        })
    ]
}

既然我们的 HTML 文件已经准备好了,我们就需要一个服务器来提供这个文件,是吧?因此,让我们使用之前安装了的 webpack-dev-server 来启动服务器。

. . .

webpack Dev Server

要配置 webpack-dev-server,我们需要打开 package.json 并添加一个新的脚本来帮助我们启动服务器。我们已经有了 dev,我们用它来构建文件。现在我们创建 start 如下所示:

json 复制代码
  "scripts": {
    "dev": "webpack --mode development",
    "start": "webpack-dev-server --mode development --open"
  }

让我们运行指令:

复制代码
npm run start

将打开默认浏览器并导航到 localhost:8080, 你会发现那里提供了 index.html,你可以打开开发者工具并看到构建的 main.js 文件也包含在其中。

. . .

使用 Loaders

如前所述,webpack 中的 loaders(加载器) 是处理各种其他文件扩展名的第三方扩展。 有很多可用于 webpack 的加载器

让我们继续在 webpack 配置文件中配置加载器。 加载器有一个奇怪的语法。 我们使用一个名为 module 的键,它由另一个名为 rules 的属性组成,它是一个加载器数组。 对于我们想要将其视为模块的每个文件,我们将其作为此 rules 数组中的对象。 每个对象都包含两个属性:test 定义文件类型,use 是由加载器组成的数组。 请注意,此处定义的加载器在 use 数组中从右向左加载。定义加载器时顺序很重要。 使用前请参阅 loader 程序的文档。

配置文件中一个 loader 的一般语法如下:

js 复制代码
module.exports = {
    module: {
        rules: [
            {
                test: /\.filename1$/,
                use: ["loader-b", "loader-a"]
            },
            {
                test: /\.filename2$/,
                use: ["loader-d", "loader-c"]
            }
        ]
    }
};

. . .

使用 CSS

为了在 webpack 中使用 CSS,我们需要两个加载器:css-loaderstyle-loader。让我们安装它们。

复制代码
npm install --save-dev css-loader style-loader

让我们在 source 目录中创建一个新文件 styles.css。添加一些样式,以便它们能在服务启动时可以在 index.html 中体现出来。接下来,我们需要将该文件包含在 index.js 中,而不是 index.html。 请记住,该教程是基于打包和学习 webpack 的。所以,我们的 index.js 文件应该如下所示。

js 复制代码
import './styles.css'
console.log('I\'m from source')

还有最后一步要做,就是在 webpack.config.js 中配置 loader。我们的配置文件应该是这样的。

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

css-loader 用于加载 css 文件, style-loader 用于在 DOM 中加载样式表。

当你使用 npm run start 重启服务后,就能从浏览器中看到更改。

. . .

使用 SASS

要使用 SASS(.scss) 文件,我们需要三个加载器,sass-loadercss-loader 以及 style-loader,还要加上 Node 需要的 sass 包。这里的 sass-loader 用于加载 SASS 文件。因为我们已经安装了另外 2 个软件包,所以现在让我们安装其余的软件包。

复制代码
npm install --save-dev sass-loader sass

让我们在 source 目录中创建一个新文件styles.scss,并添加这些行或任何基本样式。

scss 复制代码
$primary-bg: rgb(188, 220, 220);

body {
    background-color: $primary-bg;
}

让我们通过在index.js 开头 import './styles.scss'

是时候在 webpack.config.js 中添加 loader 了。loader 应该如下:

js 复制代码
module.exports = {
  module: {
      rules: [
          {
              test: /\.css$/,
              use: ['style-loader', 'css-loader']
          },
          {
              test: /\.scss$/,
              use: ['style-loader', 'css-loader', 'sass-loader']
          }
      ]
  }
};

让我们重启服务器并检查更新。

. . .

使用现代 JavaScript

webpack 本身并不知道将现代 JavaScript 转换为可以在任何浏览器中运行的兼容代码。所以它使用 Babel 来达到同样的目的。让我们安装以下软件包:@babel/core,这是实际的引擎;babel-loader,这是 webpack 需要的 loader;以及 @babel/preset-env 将 JavaScript 转换为 ES5。让我们安装依赖项:

复制代码
npm install --save-dev @babel/core babel-loader @babel/preset-env

下一步是通过在我们的根目录中创建一个名为 babel.config.json 的新文件来配置 Babel 。在这里,我们使用安装的 preset-env 来配置 Babel。

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

现在让我们将安装完成的 babel-loader 添加到我们的webpack.config.js文件中。

js 复制代码
const HTMLWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.js$/,
                exclude: /node_modules/, // 排除
                use: ['babel-loader']
            }
        ]
    }
}

现在让我们在 JS 代码中使用 ES6 及以上的语法。在我们使用 npm run dev 再次构建它之后,我们可以检查构建的 main.js 文件并看到它自动转换为浏览器兼容的代码。

. . .

在 webpack 中指定模式

webpack 有两种可用的模式:development 和 production。

development模式下,代码没有缩小。webpack 只是简单地获取我们编写的所有 JS 代码并将其加载到浏览器中,从而使其更快地重新加载应用程序。

production 模式下,webpack 应用了很多优化。它会自动使用 terser-webpack-plugin 来减小包的大小。它还设置process.env.NODE_ENVproduction。这个环境变量很有用,因为我们可以有条件地在生产和开发中执行某些操作。

使用 Webpack 的 production 模式 ,我们需要向package.json文件中添加另一个脚本。我们将命名它build。我们的脚本应该如下所示:

json 复制代码
  "scripts": {
    "dev": "webpack --mode development",
    "start": "webpack-dev-server --mode development --open",
    "build": "webpack --mode production"
  }

. . .

优化-代码分割

代码分割延迟加载 是一种避免大包的优化技术。它还避免了依赖项重复。通过使用这种机制,我们可以按需加载一段代码,例如,每当用户单击按钮时、当路由更改时,等等。被分割的代码段称为 chunk (块)。

webpack 有一个限制,即应用程序初始包的最大文件大小应小于 244 KiB。webpack 中实现代码分割的方式有 3 种:

  1. 具有多个入口点(entry points)
  2. 使用 optimization.splitChunks
  3. 动态导入

第一种方法适用于较小的项目,但在复杂的项目中不可扩展。我们在 webpack 配置文件中指定多个入口点。

使用 optimization.splitChunks

有时我们在应用程序中使用大量依赖项。例如,让我们使用流行的日期包:Moment。我选择这个包是因为它的体积有点重。让我们安装它,然后将其包含在我们的index.js文件中并运行我们的build命令。

复制代码
npm install moment

一会儿就安装成功了。现在让我们将其导入到我们的index.js文件中。

复制代码
import moment from 'moment'

现在让我们运行我们的 build 命令。

复制代码
npm install build

你将在终端上看到警告消息:

那么我们如何解决这个问题呢?这很简单。让我们添加一个名为 optimization 的 key 和其中的另一个 splitChunks 属性,如下所示:

js 复制代码
module.exports = {
    optimization: {
        splitChunks: { chunks: 'all' }
    }
}

您会发现入口点的大小大大缩小了。

动态导入

动态导入用于有条件地加载代码。这种方法在 React 和 Vue 中被广泛使用。我们可以根据用户交互或基于路由更改来加载代码。

为了进行演示,让我们向页面添加一个按钮,单击该按钮可获取文章列表。获取文章列表的代码存在于单独的文件中。它是动态导入到我们的index.js文件中的。

让我们在 source 创建一个 api.js 用于 fetch API。在这里,我们导出一个向公共 API,这个 API 返回结果。

js 复制代码
export const fetchTodos = () => {
  return fetch('https://jsonplaceholder.typicode.com/posts')
    .then(response => response.json())
    .then(json => json);
}

让我们在index.html文件中创建一个按钮,并将id属性设置为btn

html 复制代码
<button id="btn">点我加载</button>

在我们的index.js文件中,让我们通过使用一个函数来实现动态导入 api.js 文件。

js 复制代码
const getTodos = () => import ('./api')  // 等于 var getTodos = function () { return import ('./api') }

另外,让我们在文件末尾添加用于记录所获取的数据的逻辑。

js 复制代码
const btn = document.getElementById('btn');
btn.addEventListener('click', () => { 
    getTodos().then(({fetchTodos}) => {  // fetchTodos 是 api.js 中的模块
        fetchTodos().then(resp => console.log(resp)) } 
    )} 
)

此代码片段调用getTodos函数,该函数为我们导入api.js文件。然后在导入它之后,我们从中获取fetchTodos,调用该函数,然后记录成功的响应。

让我们构建文件,运行服务器 npm run start ,并确保打开开发工具并保持网络选项卡打开。你会发现点击按钮后,有一个新的 JS 文件被动态加载。

0.main.js是动态加载的文件。如果你希望这个文件有一个可读的名称,你只需要在动态导入时添加注释,如下所示:

js 复制代码
const getTodos = () => import( /* webpackChunkName: "postsAPI" */ './api')

现在,当你重复相同的过程时,它将加载一个名为postsAPI.main.js而不是0.main.js

. . .

Still Curious?

这只是对 webpack 的简单介绍。正如之前所说,本文针对的是想要开始学习 webpack 的初学者或中级开发人员。当然,webpack 中还有很多话题。如果你有兴趣了解有关 webpack 的更多信息,请参阅文档,因为那里详细解释了许多有趣的主题。

. . .

核心代码

json 复制代码
/* package.json */
"scripts": {
  "dev": "webpack --mode development",
  "start": "webpack-dev-server --mode development --open",
  "build": "webpack --mode production"
},
"dependencies": {
  "moment": "^2.27.0"
},
"devDependencies": {
  "@babel/core": "^7.11.6",
  "@babel/preset-env": "^7.11.5",
  "babel-loader": "^8.1.0",
  "css-loader": "^4.2.2",
  "html-webpack-plugin": "^4.4.1",
  "sass": "^1.26.10",
  "sass-loader": "^10.0.2",
  "style-loader": "^1.2.1",
  "webpack": "^4.44.1",
  "webpack-cli": "^3.3.12",
  "webpack-dev-server": "^3.11.0"
}
json 复制代码
/* babel.config.json */
{
  "presets": ["@babel/preset-env"]   
}
js 复制代码
/* source/api.js */
export const fetchTodos = () => {
  return fetch('https://jsonplaceholder.typicode.com/posts')
    .then(response => response.json())
    .then(json => json);
}
js 复制代码
/* source/inex.js */
import './style.scss'
import './styles.css'
import moment from 'moment'
console.log('I\'m from source')

const getTodos = () => import( /* webpackChunkName: "postsAPI" */ './api')

const btn = document.getElementById('btn');
btn.addEventListener('click', () => { 
    getTodos().then(({fetchTodos}) => {
        fetchTodos().then(resp => console.log(resp)) } 
    )} 
)
js 复制代码
/* webpack.config.js */
const HTMLWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'source', 'index.js')
    },
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'main.js'
    },
    plugins: [
        new HTMLWebpackPlugin({
            template: path.resolve(__dirname, 'source', 'index.html')
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            }
        ]
    },

    optimization: {
        splitChunks: { chunks: 'all' }
    }
}

. . .

参考 --- Learn webpack in Under 10 Minutes github.com/harshaktg/w...

相关推荐
栈老师不回家39 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙44 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
58沈剑3 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构