Webpack 快速入门指南(二) 迈不过友情╰ 2024-04-17 16:56 21阅读 0赞 ## 开发 ## 本指南继续沿用管理输出指南中的代码示例。 如果你一直跟随之前的指南,应该对一些 webpack 基础知识有着很扎实的理解。在我们继续之前,先来看看如何建立一个开发环境,使我们的开发变得更容易一些。 本指南中的工具仅用于开发环境,请不要在生产环境中使用它们! ### 使用 source map ### 当 webpack 打包源代码时,可能很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js。这通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。 source map 有很多不同的选项可用,请务必仔细阅读它们,以便可以根据需要进行配置。 对于本指南,我们使用 inline-source-map 选项,这有助于解释说明我们的目的(仅解释说明,不要用于生产环境): **webpack.config.js** const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, devtool: 'inline-source-map', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Output Management' }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } }; 现在,让我们来做一些调试,在 src/print.js 文件中,故意写一段错误的代码: export default function printMe() { cosnole.error('this line has a error'); } 运行 npm run dev,在终端并看不出异常。 现在,在浏览器打开最终生成的 index.html 文件,点击页面中的 按钮,就可以在浏览器的控制台查看显示的错误。错误应该如下: Uncaught ReferenceError: cosnole is not defined at HTMLButtonElement.printMe (print.js:2) 此错误信息具体指明了发生错误的文件(print.js)和行号(2)。这是非常有帮助的。 ### 选择一个开发工具 ### 某些文本编辑器具有安全写入功能,可能会干扰以下某些工具。 每次要编译(构建)代码时,都需要重新手动执行 npm run dev,显得很麻烦。 webpack 中有三种不同的工具,可以帮助你在代码发生变化后自动编译代码: * webpack's Watch Mode(观察模式) * webpack-dev-server * webpack-dev-middleware 多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下这三种工具。 webpack's Watch Mode(观察模式) 你可以指示 webpack "watch" 依赖图中的所有文件,如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行构建。 通过修改 package.json 文件,添加一个用于启动 webpack 的观察模式的 npm script 脚本: { "name": "webpack-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo test ok !", "watch": "webpack --watch --mode development", "dev": "webpack --mode development" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "clean-webpack-plugin": "^0.1.18", "css-loader": "^0.28.10", "csv-loader": "^2.1.1", "file-loader": "^1.1.9", "html-webpack-plugin": "github:jantimon/html-webpack-plugin", "style-loader": "^0.20.2", "webpack": "^4.0.0", "webpack-cli": "^2.0.9", "xml-loader": "^1.2.1" }, "dependencies": { "lodash": "^4.17.5" } } 现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。 在 webpack 观察文件的同时,我们先移除之前引入的错误: **src/print.js** export default function printMe() { console.log('I get called from print.js!'); } 现在,保存文件并检查终端窗口。可以看到 webpack 自动重新编译修改后的模块! 唯一的缺点是,为了看到修改后的实际效果,你需要刷新浏览器。 如果能够自动刷新浏览器就更好了,可以尝试使用 webpack-dev-server,恰好可以实现我们想要的功能。 webpack-dev-server webpack-dev-server 为你提供了一个简单的 web 服务器,并且能够实时重新加载。 安装 webpack-dev-server : npm install --save-dev webpack-dev-server 修改 webpack.config.js 文件,告诉开发服务器(dev server),在哪里查找文件: const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist' }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Output Management' }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } }; 以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。 添加 npm script 脚本,以便可以通过 npm script 直接运行开发服务器(dev server)。也就是修改项目目录中的 package.json 文件,在 scripts 键指向的值中加入如下代码: "start": "webpack-dev-server --open", 现在,我们可以在命令行中运行 **npm start** ,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码。你会发现项目中的 dist 目录 也不见了。 **说明:** webpack-dev-server 是通过 node.js 创建的 web 服务器来和 web 进行交互的。node.js 创建的 web 服务器和其他主流 web 服务器(Apache、Nginx、IIS)的功能差不多。webpack-dev-server 有很多配置选项,具体可查询 webpack 官网。 webpack-dev-middleware webpack-dev-middleware 是一个容器,它可以把 webpack 处理后的文件传递给一个服务器。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置。 接下来是一个 webpack-dev-middleware 配合 express server 的示例。 首先,安装 express 和 webpack-dev-middleware 。 npm install --save-dev express webpack-dev-middleware 接下来我们需要对 webpack 的配置文件做一些调整,以确保中间件(middleware)功能能够正确启用。主要是调整 webpack.config.js 文件中的 output 的 publicPath 属性。 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist' }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Output Management' }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/' } }; publicPath 也会在服务器脚本用到,以确保文件资源能够在 [http://localhost:3000][http_localhost_3000] 下正确访问。下一步就是设置我们自定义的 express 服务。 在项目根目录中,添加 server.js 文件: const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const app = express(); const config = require('./webpack.config.js'); const compiler = webpack(config); // Tell express to use the webpack-dev-middleware and use the webpack.config.js // configuration file as a base. app.use(webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })); // Serve the files on port 3000. app.listen(3000, function () { console.log('Example app listening on port 3000!\n'); }); 然后,在 package.json 文件中添加 npm script 脚本,以使我们更方便地运行服务。即在该文件的 scripts 中添加下面的代码: "server": "node server.js", 现在,在你的终端(命令行)执行 npm run server 。 打开浏览器,访问 [http://localhost:3000][http_localhost_3000],你应该看到你的 webpack 应用程序已经运行! ### 调整文本编辑器 ### 使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。 要在一些常见的编辑器中禁用此功能,请查看以下列表: * Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic\_save: "false"。 * IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。 * Vim - 在设置(settings)中增加 :set backupcopy=yes。 * WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消 Use "safe write"。 ### 结论 ### 现在,你已经学会了如何自动编译代码,并运行一个简单的开发服务器。接下来,进入 模块热替换(Hot Module Replacement)。 ## 模块热替换 ## 模块热替换(Hot Module Replacement)简称 HMR, 是 webpack 提供的最有用的功能之一。 它允许在运行时更新各种模块,而无需进行完全刷新。HMR 不适用于生产环境,应当只在开发环境使用。 ### 启用 HMR ### 启用 HMR 比较简单。使用 webpack 内置的 HMR 插件可以实现。 要删掉 print.js 的入口起点,因为它现在正被 index.js 模式使用。 【 如果你使用了 webpack-dev-middleware 而没有使用 webpack-dev-server,请使用 webpack-hot-middleware package 包,以在你的自定义服务或应用程序上启用 HMR。】 修改 webpack.config.js 文件: const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: { app: './src/index.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist', hot: true }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: '模块热替换' }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } }; 【 也可通过命令来修改 webpack-dev-server 的配置:webpack-dev-server --hotOnly。】 我们还添加了 NamedModulesPlugin,以便更容易查看要修补(patch)的依赖。 在命令行中运行 **npm start** 来启动 webpack-dev-server。 现在,我们来修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。 **index.js** import _ from 'lodash'; import printMe from './print.js'; function component() { var element = document.createElement('div'); var btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); btn.innerHTML = 'Click me and check the console!333'; btn.onclick = printMe; element.appendChild(btn); return element; } document.body.appendChild(component()); if (module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated printMe module!'); printMe(); }) } 随意更改 print.js 文件中 console.log 的内容。 export default function printMe() { console.log('Updating print.js...'); } 你就会在浏览器([http://localhost:8080/)][http_localhost_8080] 的控制台(Console),看到如下内容。 [HMR] Waiting for update signal from WDS... client:77 [WDS] Hot Module Replacement enabled. [WDS] App hot update... log.js:24 [HMR] Checking for updates on the server... index.js:22 Accepting the updated printMe module! print.js:2 Updating print.js... log.js:24 [HMR] Updated modules: log.js:24 [HMR] - ./src/print.js log.js:24 [HMR] App is up to date. 如此,就实现了 HMR(模块热替换)。还可以通过 Node.js API 来实现HMR(这里不讲解)。 ### 问题 ### 在刚才的示例中,如果你继续点击示例页面上的按钮,可以发现问题:控制台打印的是旧的内容。 这是因为按钮的 onclick 事件仍然绑定在旧的 printMe 函数上。 为了让它与 HRM 正常工作,我们需要调整 index.js 文件: import _ from 'lodash'; import printMe from './print.js'; function component() { var element = document.createElement('div'); var btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); btn.innerHTML = 'Click me and check the console!'; btn.onclick = printMe; element.appendChild(btn); return element; } // document.body.appendChild(component()); let element = component(); // 当 print.js 改变导致页面重新渲染时,重新获取渲染的元素 document.body.appendChild(element); if (module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated printMe module!'); // printMe(); document.body.removeChild(element); element = component(); // 重新渲染页面后,component 更新 click 事件处理 document.body.appendChild(element); }) } 这只是一个例子,还有很多其他地方容易让人犯错。幸运的是,存在很多 loader(其中一些在下面提到),使得模块热替换的过程变得更容易。 ### HMR 修改样式表 ### 借助 style-loader 的帮助,CSS 的模块热替换非常简单。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch) <style> 标签。 可以使用以下命令安装两个 loader : npm install --save-dev style-loader css-loader 接下来我们来更新 webpack 的配置,让这两个 loader 生效。 **webpack.config.js** const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: { app: './src/index.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist', hot: true }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: '模块热替换' }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } }; 在 src 目录中,添加 styles.css 文件: body { background: blue; } 在 src/index.js 中,引入该 CSS 文件。只需在 index.js 文件的开头添加如下代码: import './styles.css'; 你会发现页面有了蓝色的背景。 如果修改 styles.css 文件中的样式: body { background: red; } 页面的背景色就自动变为红色,而无需手动刷新页面。这就实现了 HMR 修改样式表。 **说明:** 在终端 **npm start** 一直处于运行中,也就是说,webpack-dev-server 一直在运行。 它是开发过程中非常好用的工具。 ### 其他代码和框架 ### 还有许多其他 loader 和示例,可以使 HMR 与各种框架和库(library)平滑地进行交互。 * React Hot Loader:实时调整 react 组件。 * Vue Loader:此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验。 * Elm Hot Loader:支持用于 Elm 程序语言的 HMR。 * Redux HMR:无需 loader 或插件!只需对 main store 文件进行简单的修改。 * Angular HMR:没有必要使用 loader!只需对主要的 NgModule 文件进行简单的修改,由 HMR API 完全控制。 接着,我们进入下一章节。 ## Tree Shaking ## tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。 ### 添加一个通用模块 ### 在我们的项目中添加一个新的通用模块文件 src/math.js,此文件导出两个函数: export function square(x) { return x * x; } export function cube(x) { return x * x * x; } 接着,更新 src/index.js 文件,为了方便,将 lodash 删除,并使用其中的一个函数。 import { cube } from './math.js'; function component() { var element = document.createElement('pre'); element.innerHTML = [ 'Hello webpack!', '5 cubed is equal to ' + cube(5) ].join('\n\n'); return element; } document.body.appendChild(component()); 我们并未从 src/math.js 模块中 import 导入 square 函数,导入了 cube 函数。 cube 函数是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export。 现在让我们运行我们的npm 脚本: npm run dev 并检查输出的 bundle (dist/app.bundle.js文件),下面只列出部分代码: ... /***/ "./src/math.js": /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: square, cube */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "square", function() { return square; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "cube", function() { return cube; }); function square(x) { return x * x; } function cube(x) { return x * x * x; } ... 你会注意到 square 函数 仍然被包含在 bundle 中。我们将在下面解决这个问题。 ### 精简输出 ### 我们已经通过 import 和 export 语法,标识出了那些“未引用代码(dead code)”,但是我们仍然需要从 bundle 中删除它们。 要做到这一点,可以使用能够删除未引用代码的压缩工具 UglifyJSPlugin(uglifyjs-webpack-plugin 插件)。 首先,安装 uglifyjs-webpack-plugin 插件。 npm install --save-dev uglifyjs-webpack-plugin 然后,将其添加到 webpack.config.js 配置文件。 const path = require('path'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); module.exports = { entry: { app: './src/index.js' }, plugins: [ new UglifyJSPlugin() ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } }; 【 也可以在命令行中使用 --optimize-minimize 标记,来使用 UglifyJsPlugin。】 运行 npm run dev,看看 bundle 的内容有没有发生改变。显然,现在整个 bundle 都已经被精简过。 虽然,在这个示例中,可能看起来没有减少很多,但是,在具有复杂的依赖树的大型应用程序上运行时,tree shaking 或许会对 bundle 产生显著的体积优化。 ### 警告 ### webpack 本身并不会执行 tree shaking。它需要依赖于像 UglifyJS 这样的第三方工具来执行未引用代码的删除工作。 有些情况下,tree-shaking 可能不会生效。 一般来说,当一个工具不能确定某些特定的代码路径(path)是否会导致副作用(side-effects)时,即使你确信它不应该存在生成的 bundle 中,但这个代码仍然会保留。常见的情况有:从第三方模块中调用一个函数,webpack 或 压缩工具(minifier)无法检查此模块;从第三方模块导入的函数被重新导出,等等。 还有其他工具,如 webpack-rollup-loader,也能执行 tree shaking。 ### 结论 ### 为了学会使用 tree shaking,只需遵守两点: * 使用 ES2015 模块语法(即 import 和 export)。 * 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。 你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。 如果你对优化输出很感兴趣,请进入下一章节——生产环境构建。 [http_localhost_3000]: http://localhost:3000 [http_localhost_8080]: http://localhost:8080/%EF%BC%89
还没有评论,来说两句吧...