目标:使用webpack从零开始配置一个react项目
概念
借用官网的解释:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
要理解webpack是什么,要从两个词出发:“模块”和“打包”。
为什么要打包?
以一个html页面为例,在页面中通过script标签引入了3个JavaScript文件a.js,b.js和c.js,每个文件中分别定义了一个函数并导出给外部用。并且它们之间有一定的依赖关系,c.js依赖于b.js,b.js依赖于a.js。
因为有3个独立的js文件,所以在加载的时候浏览器需要发送三次http请求来获取这三个文件,然后依次执行其中的代码,如果其中有一个文件因为网络问题而延误了时间,那么整个页面的显示也会被延误。当我们的项目逐渐变大,有几十个到上百个JavaScript文件的时候,那问题会更严重,不但有延迟问题,还会遇到很难维护的问题。所以需要尽可能的合并文件,减少http请求,这个合并的过程就是打包,通常将几个分散的有依赖关系的文件打包为一个文件。
通常情况下,为了提高开发效率,我们会使用诸如ES6,less等,就需要在运行时做代码的转换,使得其可以在对应的终端上正常执行,这个过程如果每次都手动通过工具做转换的话非常的费时,理想的情况是,我们可以使用这些具有新特性的东西,又可以通过某种工具自动的完成转换,进而提升开发效率,这个自动转换的过程也是打包的过程。
什么是模块
模块可以理解为一个单独的文件,或者一个方法,每一个模块都是一个单独的作用域, 也就是说, 在该模块内部定义的变量, 无法被其他模块读取, 除非定义为global(浏览器中为window)对象的属性。模块可以被复用,模块之间可以被相互引用,比如一个处理浮点数加法的方法,就是一个模块,可能在多个地方使用,使用的地方直接导入这个方法即可。
webpack的核心功能就是打包,下面我们会针对不同的文件类型做处理,充分发挥webpack的打包模块的作用。
配置
以下内容将从以下几个方面展开:
- 初始化webpack配置
- 配置css,使用预处理器
- 配置html,自动引入打包后的文件
- 使用 Babel 来支持 ES 新特性
- 处理图片
- 本地搭建服务器
初始化项目
1 | mkdir react-fe |
如果对于生成的package.json中有不理解的,可以查看这篇文章package.json 知多少?
package.json中添加脚本配置:
1 | "scripts": { |
在项目目录下新建src文件,用于存放源文件,新建/src/index.js,内容任意
1 | console.log('hello world'); |
执行yarn build,会新增一个dist目录,里面是打包后的文件
1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){console.log("hello world")}]); |
webpack 运行时默认读取项目下的 webpack.config.js 文件作为配置;这个配置其实是一个node js的脚本,脚本对外暴露一个配置对象,webpack通过这个对象来读取相关的一些配置,因为是node js的脚本,所以可以使用任何node模块。通常一个项目会分为开发模式和生产模式,所以我们创建一个目录build,此目录下的文件均和构建相关。如下:
1 | |- build (打包配置目录) |
webpack.config.js配置如下:
1 | const path = require('path'); |
配置webpack的mode, 枚举值有production,development, none,在webpack4中配置了mode,相当于使用DefinePlugin设置了NODE_ENV,这个值用于区分生产模式还是开发模式,后续会告诉webpack使用哪种模式开启内置优化,mode值的不同,build时默认的配置也会不同,有了默认配置,就不需要手动启用插件了。
选项 | 描述 |
---|---|
development | 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。 |
production | 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin. |
配置webpack.dev.config.js
1 | var base = require('./webpack.config'); |
配置webpack.prod.config.js
1 | var base = require('./webpack.config'); |
添加npm scripts
1 | "scripts": { |
配置环境变量
cross-env是一个运行跨平台设置和使用环境变量的脚本,比如我们想要分析打包后的各个文件大小以及依赖关系,可以添加”webpack-bundle-analyzer”,这个包不是在每一次打包的时候都用到,所以,单独添加一个条件,只有在需要分析的时候才使用这个包
1 | yarn add cross-env webpack-bundle-analyzer -D |
只设置 NODE_ENV,则不会自动设置 mode
配置css,使用预处理器
webpack 中提供一种处理多种文件格式的机制,便是使用 loader。我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块。
loader本身是一个导出为function的node模块;
在没有任何loader处理的情况下,webpack默认只能处理js文件,最终输出js文件,而loader的作用就是把非js文件转换为webpack可以处理的js文件
将内联图像转换为data URL
css-loader
负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明;将样式代码处理为js数组,样式代码处理为字符串
1 | yarn add css-loader -D |
新建文件,目录如下:
1 | |- src |
执行 yarn build
可以看到打包后的main.js中,css-loader将样式代码处理成了js数组,并且我们的样式代码被处理成了字符串。
1 | function(n, t, o) { |
经过css-loader处理完的文件并没有应用到页面上,如果想要样式生效,还需要style-loader的处理
1 | yarn add style-loader -D |
style-loader将css-loader返回的样式数组一顿操作插入到html head,然后自己返回了一个空对象。
loader处理的时机:在import或”加载“时预处理文件,类似于其他构建工具中的task
loader处理顺序:从右向左执行,支持链式传递,链中的每个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 期望 JavaScript。
less-loader
通常情况下,我们会使用预处理器来编写css,比如使用less或者sass,这样可以大大提高开发效率,下面以less为例,将原来的css文件修改为less文件,并且内容修改如下:
1 | // index.less |
postcss-loader
另外常用的处理样式的方式是,使用postcss,它是一种对css编译的工具,类似babel对js的处理,常见的功能如:
- 使用下一代css语法
- 自动补全浏览器前缀: autoprefixer
- 自动把px代为转换成rem: postcss-pxtorem
- css 代码压缩等等
postcss 只是一个工具,本身不会对css一顿操作,它通过插件实现功能。
1 | // 安装loader和plugin |
开发中还有一个常用的功能是,比如对某个依赖的UI库做单位转换,也可以单独配置plugin,比如我们项目中设计稿的尺寸是1080,但是依赖的库是750的,并且是px为单位的,为了兼容我们的项目,就要对依赖库做单位转换。
1 | module: { |
通常一个成熟的项目,我们不会使用style-loader的方式将结果插入到head中,也不会自己手动去修改html中引入打包后文件的路径。下面将配合plugin使用
添加plugin,分离html,css,js
webpack的plugin比loader强大,通过钩子可以涉及整个构建流程,可以做一些在构建范围内的事情;理论上可以干涉 webpack 整个构建流程,可以在流程的每一个步骤中定制自己的构建需求。
plugin:构建流程中处理构建任务,可以这么理解,模块代码的转换工作由loader处理,除此之外的任何其他工作都可以由plugin完成。
常用的两个插件:
- html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件,这对生成的文件使用了hash的情况非常有用
- mini-css-extract-plugin 将js文件中引用的样式单独抽离成css文件,需要结合css-loader一起使用,另外,这个插件使用后就不需要style-loader
1 | yarn add html-webpack-plugin mini-css-extract-plugin -D |
执行 yarn build的结果
1 | |- dist |
作为普通的html开发,你可能会需要从网上下载一些js文件而不是使用cdn的方式引入,比如用于适配移动端的flexiable,这种文件不需要经过babel再次进行处理,所以打包后直接在html中引入即可
1 | yarn add copy-webpack-plugin -D |
使用 Babel 来支持 ES 新特性
现在的项目中我们一般会使用ES6开发,所以需要用babel处理,关于babel的使用,可以参考babel学习
1 | yarn add @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D |
处理图片,压缩文件
处理图片,常使用的两个loader是url-loader和file-loader,其中 url-loader 是将图片转换成一个 DataURL,然后打包到 JavaScript 代码中,这对小的图片来说是不错的处理方式,可是大图片这种处理方式就不适用了,无疑会增大js的体积,通过使用file-loader将文件处理后输出到目录中。
1 | yarn add file-loader url-loader -D |
到此为止,我们已经实现了如何使用webpack来搭建一个项目了,即使不适用诸如react, vue这样的框架,也是可以正常启动打包项目的。
webpack搭建本地服务器(express + webpack-dev-middleware)
开发过程中,我们希望边修改就能看到更新后的结果,所以本地开发启动服务热更新很重要,不然就得每次去build然后刷新看结果。
webpack为我们提供了实现本地热更新的插件:webpack-dev-server,使用和配置很简单,可参照官方文档进行配置。但是本篇我们不使用这个。通过使用express服务器,可以进行更多的扩展,结合使用其他的中间件来响应http请求及其他的功能,扩展性更好,较为灵活。
开启了 hot 功能的 webpack 会往我们应用的主要代码中添加 WS 相关的代码,用于和服务器保持连接,等待更新动作。
1 | // webpack.dev.config.js |
按照上面的配置,可以实现,更新本地文件,浏览器自动刷新,但是有个问题,比如我们在页面输入了两个值,自动刷新后,刚刚输入的值就没有了,对复杂的页面操作来说,每次更新文件都需要重新进行一遍操作,是很影响效率的,造成这种现象的原因是热更新不能存储state的状态,使用react-hot-loader可以解决这个问题。
下一篇文章介绍react的引入。