从0到1构建webpack多页面多环境应用
Webpack凭借强大的功能,成为最流行和最活跃的打包工具,也是面试时高级程序员必须掌握的“软技能”;笔者结合在项目中的使用经验,介绍webpack的使用;本文是入门篇,主要介绍webpack的入口、输出和各种loader、plugins的使用以及开发环境的搭建。本文所有的demo代码均在https://github.com/acexyf/WebpackDemo
一、概念
来看一下官网对webpack的定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
首先webpack是一个静态模块打包器,所谓的静态模块,包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。再次白piao一下官网的图,生动的描述了这个过程:
二、4个核心概念
提到webpack,就不得不提webpack的四个核心概念
- 入口(entry):指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
- 输出(output):在哪里输出它所创建的 bundles
- loader:让 webpack 能够去处理那些非 JavaScript 文件
- 插件(plugins):用于执行范围更广的任务
三、你的第一个打包器
1、我们首先在全局安装webpack:
npm install webpack webpack-cli –g
webpack可以不使用配置文件,直接通过命令行构建,用法如下:
webpack [] -o
这里的entry和output就对应了上述概念中的入口和输入,我们来新建一个入口文件:
有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js:
webpack index.js -o dist/bundle.js
这样webpack就会在dist目录生成打包后的文件。
我们也可以在项目目录新建一个html引入打包后的bundle.js文件查看效果。
2、配置文件
命令行的打包方式仅限于简单的项目,如果我们的项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
webpack [–config webpack.config.js]
配置文件默认的名称就是webpack.config.js
,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的文件,通过--config
来进行切换:
//生产环境配置
webpack --config webpack.prod.config.js
//开发环境配置
webpack --config webpack.dev.config.js
多种配置类型
config配置文件通过module.exports导出一个配置对象:
除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们在打包线上正式环境和线上开发环境可以通过env进行区分:
另外还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:
3、入口
正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:
它是下面的简写:
但是我们一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了,代码在demo2中:
有时候我们一个项目可能有不止一个页面,需要将多个页面分开打包,entry支持传入对象的形式,代码在demo3中:
这样webpack就会构建三个不同的依赖关系。
4、输出
output选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry,但是只能配置一个output:
这里我们配置了一个单入口,输出也就是bundle.js;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename
,即多个文件资源有相同的文件名称;webpack提供了占位符
来确保每一个输出的文件都有唯一的名称:
这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:
5、Module、Chunk和Bundle的概念
在这里引入Module、Chunk和Bundle的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module是经常出现在我们的代码中,比如module.exports;而Chunk经常和entry一起出现,Bundle总是和output一起出现。
- module:我们写的源码,无论是commonjs还是amdjs,都可以理解为一个个的module
- chunk:当我们写的module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk文件,webpack 会对这些chunk文件进行一些操作
- bundle:webpack处理好chunk文件后,最后会输出bundle文件,这个bundle文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
我们通过下面一张图更深入的理解这三个概念:
总结:
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的bundle。
6、hash、chunkhash、contenthash
理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;
- hash:是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
- chunkhash:跟入口文件的构建有关,根据入口文件构建对应的chunk,生成每个chunk对应的hash;入口文件更改,对应chunk的hash值会更改。
- contenthash:跟文件内容本身相关,根据文件内容创建出唯一hash,也就是说文件内容更改,hash就更改。
7、模式
在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。
开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。
生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。
相信很多童鞋都曾有过疑问,为什么这边DefinePlugin
定义环境变量的时候要用JSON.stringify("production")
,直接用"production"
不是更简单吗?
我们首先来看下JSON.stringify(“production”)生成了什么;运行结果是""production""
,注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"
多嵌套了一层引号。
我们可以简单的把DefinePlugin这个插件理解为将代码里的所有process.env.NODE_ENV替换为字符串中的内容。假如我们在代码中有如下判断环境的代码:
这样生成出来的代码就会编译成这样:
但是我们代码中可能并没有定义production变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify来包裹一层:
这样编译出来的代码就没有问题了。
8、自动生成页面
在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:
npm install --save-dev html-webpack-plugin
在demo3中,我们生成了三个不同的bundle.js
,我们希望在三个不同的页面能分别引入这三个文件,如下修改config文件:
我们以index.html作为模板文件,生成home、list、detail三个不同的页面,并且通过chunks分别引入不同的bundle;如果这里不写chunks,每个页面就会引入所有生成出来的bundle。
html-webpack-plugin
还支持以下字段:
上面设置title后需要在模板文件中设置模板字符串:
四、loader
loader用于对模块module的源码进行转换,默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。
module.rules
允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader,loader的代码均在demo4中。
我们可以看到rules属性值是一个数组,每个数组对象表示了不同的匹配规则;test属性时一个正则表达式,匹配不同的文件后缀;use表示匹配了这个文件后调用什么loader来处理,当有多个loader的时候,use就需要用到数组。
多个loader支持链式传递
,能够对资源进行流水线处理,上一个loader处理的返回值传递给下一个loader;loader处理有一个优先级,从右到左,从下到上
;在上面demo中对css的处理就遵从了这个优先级,css-loader先处理,处理好了再给style-loader;因此我们写loader的时候也要注意前后顺序。
1、css-loader和style-loader
css-loader和style-loader从名称看起来功能很相似,然而两者的功能有着很大的区别,但是他们经常会成对使用;安装方法:
npm i -D css-loader style-loader
css-loader
用来解释@import
和url()
;style-loader
用来将css-loader
生成的样式表通过<style>
标签,插入到页面中去。
然后在入口文件中将index.css引入,就能看到打包的效果,页面中插入了三个style标签,代码在demo4:
2、sass-loader和less-loader
这两个loader看名字大家也能猜到了,就是用来处理sass和less样式的。安装方法:
npm i -D sass-loader less-loader node-sass
在config中进行配置,代码在demo4:
3、postcss-loader
都0202年了,小伙伴肯定不想一个一个的手动添加-moz、-ms、-webkit等浏览器私有前缀;postcss提供了很多对样式的扩展功能;啥都不说,先安装起来:
npm i -D postcss-loader
老规矩,还是在config中进行配置:
正当我们兴冲冲的打包看效果时,发现样式还是老样子,并没有什么改变。
这是因为postcss主要功能只有两个:第一就是把css解析成JS可以操作的抽象语法树AST,第二就是调用插件来处理AST并得到结果;所以postcss一般都是通过插件来处理css,并不会直接处理,所以我们需要先安装一些插件:
npm i -D autoprefixer postcss-plugins-px2rem cssnano
在项目根目录新建一个.browserslistrc文件。
>0.25%
last 2 versions
我们将postcss的配置单独提取到项目根目录下的postcss.config.js:
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
有了autoprefixer插件,我们打包后的css就自动加上了前缀。
4、babel-loader
兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的js代码不能运行在IE10或者IE11上,然后尝试着引入各种polyfill;babel的出现给我们提供了便利,将高版本的ES6甚至ES7转为ES5;我们首先安装babel所需要的依赖:
npm i -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm i -S @babel/runtime
然后在config添加loader对js进行处理:
{
rules:[{
test: /\.js/,
use: {
loader: 'babel-loader'
}
}]
}
同样的,我们把babel的配置提取到根目录,新建一个.babelrc
文件:
{
"presets":[
"@babel/preset-env"
],
"plugins":[
"@babel/plugin-transform-runtime"
]
}
我们可以在index.js中尝试写一些es6的语法,看到代码会被转译成es5,代码在demo4中。由于babel-loader的转译速度很慢,在后面我们加入了时间插件后可以看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,我们对config进行改进,
{
rules:[{
test: /\.js/,
use: {
loader: 'babel-loader'
},
include: [path.resolve(_dirname, 'src')]
}]
}
正则上使用$来进行精确匹配,通过exclude将node_modules中的文件进行排除,include将只匹配src中的文件;可以看出来include的范围比exclude更缩小更精确,因此也是推荐使用include。
5、file-loader和url-loader
file-loader和url-loader都是用来处理图片、字体图标等文件;url-loader工作时分两种情况:当文件大小小于limit参数,url-loader将文件转为base-64编码,用于减少http请求;当文件大小大于limit参数时,调用file-loader进行处理;因此我们优先使用url-loader,首先还是进行安装,安装url-loader之前还需要把file-loader先安装:
npm i file-loader url-loader -D
接下来还是修改config:
我们在css中给body添加一个小于10k的居中背景图片:
打包后查看body的样式可以发现图片已经被替换成base64格式的url了,代码在demo4。
6、html-withimg-loader
如果我们在页面上引用一个图片,会发现打包后的html还是引用了src目录下的图片,这样明显是错误的,因此我们还需要一个插件对html引用的图片进行处理:
npm i -D html-withimg-loader
老样子还是在config中对html进行配置:
{
rules:[{
test: /\.(htm|html)$/,
use: {
loader: 'html-withing-loader'
}
}]
}
然鹅,打开页面发现却是这样的:
这是因为在url-loader中把每个图片作为一个模块来处理了,我们还需要去url-loader中修改:
use: {
loader: 'url-loader',
options:{
limit: 10240,
esModule: false
}
}
这样我们在页面上的图片引用也被修改了,代码在demo4中。