为什么使用webpack?
现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法。
- 处理模块化:*CSS 和 JS 的模块化语法,目前都无法被浏览器兼容。因此,开发环境可以使用既定的模块化语法,但是需要构建工具将模块化语法编译为浏览器可识别形式。例如,使用 webpack、Rollup 等处理 JS 模块化。
- 编译语法:编写 CSS 时使用 Less、Sass,编写 JS 时使用 ES6、TypeScript 等。这些标准目前也都无法被浏览器兼容,因此需要构建工具编译,例如使用 Babel 编译 ES6 语法。
- 代码压缩:将 CSS、JS 代码混淆压缩,为了让代码体积更小,加载更快。
这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为webpack类的工具的出现提供了需求。
什么是webpack
webpack可以看做是模块打包机:它做的事情是,分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。
webpack 是一个 JS 代码模块化的打包工具,藉由它强大的扩展能力,随着社区的发展,逐渐成为一个功能完善的构建工具。
webpack和gulp对比
Webpack和gulp没有太多的可比性,gulp是一种能够优化前端的开发流程的工具,而webpack是一种模块化的解决方案,不过webpack的优点使得webpack在很多情景下可以替代gulp类的工具。
gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译、组合压缩等任务的具体步骤,工具可以自动化完成这些任务。
webpack的工作方式是:把项目当作一个整体,通过一个给定的主文件(如:index.js),webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,把我们项目中使用到的多个代码模块(可以是不同文件类型),打包构建成项目运行仅需要的几个静态文件。
webpack处理速度更快更直接,能打包更多不同类型的文件。
webpack基础使用
安装和使用
使用npm或者yarn来安装webpack,可以作为一个全局的命令来使用:
1 | yarn global add webpack webpack-cli |
webapck-cli是使用webapck的命令行工具,4.x版本之后不再作为webpack的依赖,使用时需要单独安装这个工具。
实际项目中,我们通常会把webpack作为项目的开发依赖来安装使用,这样可以指定项目中使用的webpack版本,便于多人协同开发。
1 | yarn add webpack -D |
这样 webpack 会出现在 package.json 中,我们再添加一个 npm scripts:
1 | "scripts": { |
然后创建./src/index.js文件,写任意JS代码,执行yarn build命令,这时候发现新增一个dist目录,里面有一个webpack构建好的main.js 文件
因为是作为项目依赖进行安装,所以不会有全局的命令,npm/yarn 会帮助我们在当前项目依赖中寻找对应的命令执行,如果是全局安装的 webpack,直接执行 webpack –mode production 就可以。
webpack 4.x 的版本可以零配置就开始进行构建,但是功能并不全面,还是需要配置文件。
基本概念
- 入口
在多个代码模块中会有一个起始的.js文件,这个就是webpack构建的入口。webpack会读取这个文件,并从它开始解析依赖,然后进行打包。一开始我们使用 webpack 构建时,默认的入口文件就是 ./src/index.js。
我们常见的项目中,如果是单页面应用,那么可能入口只有一个;如果是多个页面的项目,那么经常是一个页面会对应一个构建入口。
入口可以使用 entry 字段来进行配置,webpack 支持配置多个入口来进行构建:
1 | module.exports = { |
- loader
webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。
例如:在没有添加额外插件的情况下,webpack 会默认把所有依赖打包成 js 文件,如果入口文件依赖一个 .hbs 的模板文件以及一个 .css 的样式文件,那么我们需要 handlebars-loader 来处理 .hbs 文件,需要 css-loader 来处理 .css 文件(这里其实还需要 style-loader),最终把不同格式的文件都解析成 js 代码,以便打包后在浏览器中运行。
当我们需要使用不同的 loader 来解析处理不同类型的文件时,我们可以在 module.rules 字段下来配置相关的规则,例如使用 Babel 来处理 .js 文件:
1 | module: { |
loader 是 webpack 中比较复杂的一块内容,它支撑着 webpack 来处理文件的多样性。
- plugin
在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。可以这么理解,模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。通过添加我们需要的 plugin,可以满足更多构建中特殊的需求。
例如,要使用压缩 JS 代码的 uglifyjs-webpack-plugin 插件,只需在配置中通过 plugins 字段添加新的 plugin 即可:
1 | const UglifyPlugin = require('uglifyjs-webpack-plugin') |
除了压缩 JS 代码的 uglifyjs-webpack-plugin,常用的还有定义环境变量的 DefinePlugin,生成 CSS 文件的 ExtractTextWebpackPlugin 等。plugin 理论上可以干涉 webpack 整个构建流程,可以在流程的每一个步骤中定制自己的构建需求。
- 输出
webpack 的输出即指 webpack 最终构建出来的静态文件,可以看看上面 webpack 官方图片右侧的那些文件。当然,构建结果的文件名、路径等都是可以配置的,使用 output 字段:
1 | module.exports = { |
我们一开始直接使用 webpack 构建时,默认创建的输出内容就是 ./dist/main.js。
一个简单的webpack配置
在项目中创建一个 webpack.config.js 文件:
1 | const path = require('path') |
基本的前端开发环境
日常前端的开发环境有哪些需求?
- 构建发布需要的 HTML、CSS、JS 文件
- 清理输出文件夹
- 使用 CSS 预处理器来编写样式
- 处理和压缩图片
- 使用 Babel 来支持 ES 新特性
- 本地提供静态服务以方便开发调试
- 热替换(HMR)
关联Html
webpack 默认从作为入口的 .js 文件进行构建(更多是基于 SPA 去考虑),但通常一个前端项目都是从一个页面(即 HTML)出发的,最简单的方法是,创建一个 HTML 文件,使用 script 标签直接引用构建好的 JS 文件,如:
1 | <script src="./dist/bundle.js"></script> |
但是,如果我们的文件名或者路径会变化,例如使用 [hash] 来进行命名,那么最好是将 HTML 引用路径和我们的构建结果关联起来,这个时候我们可以使用 html-webpack-plugin,html-webpack-plugin 是一个独立的 node package,所以在使用之前我们需要先安装它,把它安装到项目的开发依赖中:
1 | yarn add html-webpack-plugin -D |
然后在 webpack 配置中,将 html-webpack-plugin 添加到 plugins 列表中,配置好之后,构建时 html-webpack-plugin 会为我们创建一个 HTML 文件,其中会引用构建出来的 JS 文件。实际项目中,默认创建的 HTML 文件并没有什么用,我们需要自己来写 HTML 文件,可以通过 html-webpack-plugin 的配置,传递一个写好的 HTML 模板:
1 | const HtmlWebpackPlugin = require('html-webpack-plugin') |
这样,通过 html-webpack-plugin 就可以将我们的页面和构建 JS 关联起来,回归日常,从页面开始开发。如果需要添加多个页面关联,那么实例化多个 html-webpack-plugin, 并将它们都放到 plugins 字段数组中就可以了。
清理输出文件夹
在每次build之前,我们希望将现有存在的输出路径清除。安装html-webpack-plugin:
1 | yarn add clean-webpack-plugin -D |
1 | const HtmlWebpackPlugin = require('clean-webpack-plugin') |
构建css
我们编写 CSS,并且希望使用 webpack 来进行构建,为此,需要在配置中引入 loader 来解析和处理 CSS 文件:
1 | module.exports = { |
我们创建一个 index.css 文件,并在 index.js 中引用它,然后进行构建。
1 | import "./index.css" |
可以发现,构建出来的文件并没有 CSS,先来看一下新增两个 loader 的作用:
- css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明;
- style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。
1 | { |
经由上述两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js 一起打包了。如果需要单独把 CSS 文件分离出来,我们需要使用插件。
- 如果当前项目是webpack3.x版本,使用extract-text-webpack-plugin;
- 如果当前项目是webpack4.x版本(但已有extract-text-webpack-plugin配置),可以继续用extract-text-webpack-plugin,但必须用对应的beta版本,且这个beta版本不支持生成hash;
- 如果当前项目是webpack4.x版本且是新项目,使用mini-css-extract-plugin。
1 | yarn add extract-text-webpack-plugin@next -D |
1 | // const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
css预处理器
通常我们会使用 Less/Sass 等 CSS 预处理器,webpack 可以通过添加对应的 loader 来支持,以使用 Sass 为例,我们可以在官方文档中找到对应的 loader。需要在上面的 webpack 配置中,添加一个配置来支持解析后缀为 .scss 的文件:
1 | yarn add sass-loader node-sass -D |
1 | module.exports = { |
处理图片文件
在前端项目的样式中总会使用到图片,虽然我们已经提到 css-loader 会解析样式中用 url() 引用的文件路径,但是图片对应的 jpg/png/gif 等文件格式,webpack 处理不了。是的,我们只要添加一个处理图片的 loader 配置就可以了,现有的 file-loader 就是个不错的选择。
file-loader 可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回。配置很简单,在 rules中添加一个字段,增加图片类型文件的解析配置。
主要有以下loader用于处理图片:
- file-loader,用于将图片转为连接
- url-loader,对小图片直接Base64编码,对大图片通过file-loader进行处理
- image-webpack-loader,对各种图片进行压缩
1 | module.exports = { |
可以看到,我们会对所有的图片进行压缩,压缩之后的图片如果小于8KB,那么将直接转为Base64编码,否则通过URL的形式连接图片。
使用babel
Babel 是一个让我们能够使用 ES 新特性的 JS 编译工具,我们可以在 webpack 中配置 Babel,以便使用 ES6、ES7 标准来编写 JS 代码。
1 | yarn add @babel/core @babel/preset-env babel-loader -D |
1 | module.exports = { |
Babel 的相关配置可以在目录下使用 .babelrc 文件来处理
1 | { |
启动静态服务
至此,我们完成了处理多种文件类型的 webpack 配置。我们可以使用 webpack-dev-server 在本地开启一个简单的静态服务来进行开发。
在项目下安装 webpack-dev-server,然后添加启动命令到 package.json 中:
1 | "scripts": { |
yarn start就可以访问 http://localhost:8080/ 来查看页面了,默认是访问 index.html,如果是其他页面要注意访问的 URL 是否正确。
devTool(配置生成 sourcemap 的方式)。
7种SourceMap模式:
模式 | 解释 |
---|---|
eval | 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL. |
source-map | 生成一个SourceMap文件. |
hidden-source-map | 和 source-map 一样,但不会在 bundle 末尾追加注释. |
inline-source-map | 生成一个 DataUrl 形式的 SourceMap 文件. |
eval-source-map | 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap. |
cheap-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的 sourcemap) |
cheap-module-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。 |
⚠️ webpack 不仅支持这 7 种,而且它们还是可以任意组合上面的eval、inline、hidden关键字,就如文档所说,你可以设置 souremap 选项为 cheap-module-inline-source-map。
⚠️ 如果你的modules里面已经包含了SourceMaps,你需要用source-map-loader 来和合并生成一个新的 SourceMaps。
开发模式推荐:cheap-module-eval-source-map
生产环境推荐:cheap-module-source-map
推荐原因:
- 使用 cheap 模式可以大幅提高 souremap 生成的效率。大部分情况我们调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。
- 使用 eval 方式可大幅提高持续构建效率。官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快。
- 使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)。
- 使用 eval-source-map 模式可以减少网络请求。这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。
热替换(HMR)
在开发过程中,我们希望修改代码之后及时在浏览器中看到效果,因此一般的开发服务器会监听文件的变化,如果有变化便及时刷新整个页面。这种方式并不好,因为页面的数据也会随着刷新而清零,因此有了HMR,它允许我们只刷新页面中修改的那部分。
要启用HMR,首先在webpack.config.js中加入:
1 | ... |
然后引入HotModuleReplacementPlugin插件:
1 | ... |
当然如果要使用HMR,我们自己的代码还需要做一些修改,不过这些修改已经在很多loader里面实现了,比如vue-loader或者style-loader。
对于style-loader而言,由于我们项目中已经引入该loader,意味着我们在上述HMR配置完成之后,便自动地获得了css的HMR功能。
参考资料:
https://segmentfault.com/a/1190000006178770#articleHeader3
https://www.jianshu.com/p/b30cf56a431f