模块化确实很好地解决了复杂应用开发过程当中的代码组织问题,但是随着我们引入模块化,我们的应用又会产生许多新的问题:
无容置疑,模块化是必要的,不过我们需要在原有的基础之上引入更好的方案或者工具去解决上面几个问题。让开发者在开发阶段可以继续享受模块化所带来的优势,又不必担心模块化对生产环境所产生的一些影响。
我们希望它们能够满足:
由此,前端模块打包工具就诞生了。
前端领域有一些工具就很好地解决了以上这几个问题,其中最主流的就是Webpack、Parcel和Rollup。
我们就拿Webpack为例,它的一些和新特性就很好地满足了上面我们所说的需求。
Module bundler
),它本身就可以解决模块化JavaScript代码打包的问题。我们通过Webpack就可以将零散的代码打包到同一个JS文件当中。对于有环境兼容问题的代码,我们就可以在打包的过程当中通过模块加载器(Loader
)对其进行编译转换。Code Splitting
)的能力,它能够将应用当中所有的代码都按照我们的需要去打包。我们可以把应用加载过程当中初次运行时所必需的模块打包到一起,对于其他模块再单独存放,等到应用工作过程当中实际需要到某个模块再异步去加载这个模块,从而实现增量加载。Asset Module
)的问题,Webpack支持在JavaScript当中以模块化的方式载入任意类型的资源文件。这是Webpack解决了我们上边所说的这些需求,其他打包工具也都是类似的。总的来说,所有打包工具都是以模块化为目标。
打包工具解决的是前端整体的模块化,并不是指JavaScript模块化。它就可以让我们在开发阶段更好的去享受模块化所带来的优势,同时又不必担心模块化对生产环境所产生的影响,这就是模块化工具的作用。
Webpack作为目前最主流的前端模块打包器,提供了一整套的前端项目模块化方案,而不仅仅是局限于只对JavaScript的模块化。通过Webpack提供的前端模块化方案就可以很轻松地对前端项目开发过程中涉及到的所有的资源进行模块化。
接下来通过我们来通过一个小案例先来了解一下Webpack的基本使用。项目代码
1.我们首先安装项目依赖(自定义的项目可以忽略)
yarn
2.安装serve
yarn add serve --dev
3.通过serve运行项目(正常运行)
yarn serve .
4.引入Webpack,使用项目代码可以直接yarn安装依赖(引用项目代码的话可以忽略)
yarn add webpack webpack-cli --dev
5.打包src下的js代码
yarn webpack
6.修改html引用的js路径,去除type=“module”(引用的项目代码已更改)
7.将webpack命令定义到package.json当中(引用的项目代码已更改)
{
...
"scripts": {
"build": "webpack"
},
...
}
8.使用build启动打包
yarn build
Webpack4以后的版本支持零配置的方式直接启动打包,这个打包过程会按照约定将 'src/index.js' -> 'dist/main.js'
,但是很多时候我们都需要自定义这些路径。项目代码
例如这个案例当中,入口就是src下的main.js,这时我们就需要专门为webpack添加专门的配置文件webpack.config.js
,这个文件是一个运行在node环境当中的js文件,也就是说需要按照Common JS
的方式去编写代码
const path = require('path')
module.exports = {
entry: './src/main.js', // 输入
output: {
filename: 'bundle.js', // 输出
path: path.join(__dirname, 'output') // 输出目录(绝对路径 通过path转换)
}
}
Webpack4新增了一个工作模式的用发,这种用法大大简化了Webpack配置的复杂程度。可以理解成针对不同环境的几组预设配置。项目代码
我们注意到,打包过程中如果不设置mode
,终端会打印出一段配置警告,大致意思是说我们没有去设置一个叫做mode
的属性,Webpack会默认使用production
模式去工作。在这个模式下面,Webpack会自动去启动一些优化插件,例如自动压缩代码,这对实际生产环境是非常友好的,但是打包结果无法阅读。
我们可以通过cli
参数去指定打包模式,给webpack命令传入--mode
参数。这个属性有三种取值:
production
生产模式下,会自动启动优化优化打包结果。development
开发模式下,Webpack会自动优化打包速度,会添加一些调试过程中需要的辅助到代码当中none
模式下,Webpack就是运行最原始状态的打包,不会做任何额外的处理。具体这三种模式的差异可以在官方文档中找到。
除了使用cli
指定工作模式,我们还可以到配置文件中去设置工作模式
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
mode: 'development',
...
}
打开项目代码打包过后的bundle.js文件,通过ctrl+K ctrl+0把代码折叠起来以便我们对整体结构的了解
我们可以看到,整体生成的代码是一个立即执行函数,这个函数是Webpack的工作入口,它接收modules参数,调用时传入一个数组。
展开这个数组,数组当中的每一个元素都是一个参数列表相同的函数,对应的函数就是源代码中的模块。也就是说,每一个模块最终都会被包裹到这样一个函数当中,从而去实现模块的私有作用域。
我们再来展开Webpack的工作入口函数,这个函数内部最开始先定义了一个对象用于去存放(缓存)我们加载过的模块。紧接着定义了一个require函数,顾名思义,这个函数就是用来加载模块的。再往后就是在require这个函数上挂在了一些其他的数据和一些工具函数。函数执行到最后它调用了require这个函数,传入0开始去加载模块。(这个地方的模块id实际就是上面的模块数组当中的元素下标,也就是说这里才开始加载源代码当中所谓的入口模块)
为了可以更好的理解,我们把它运行起来,通过浏览器的开发工具来单步调试一下
在函数一开始运行的时候,它接受到的应该是两个模块所对应的两个函数
在这个位置,它加载了id为0的模块,我们进入到这个require函数内部
require函数内部先去判断模块有没有被加载过,如果加载了,就从缓存里面读,如果没有就创建一个新的对象
紧接着调用了这个模块相对应的函数,把刚刚创建的模块对象还有导出成员对象以及require函数传入进去。这样在模块内部就可以使用module.xeports导出成员,通过Webpack的require载入模块
我们进来,在模块内部,它先去调用了一个r函数,这个r函数内部作用就是用来给我们在导出对象上去添加一个标记,我们进去看一下
进来过后,它实际上就是在导出对象上定义了一个__esModule的一个标记,定义完成过后这个导出对象上面就有了这样一个标记
用来对外界表面这是一个ES Module,紧接着往下又调用了这个require函数,此时传入的id是1,也就是说去加载第一个模块,这个模块实际上就是我们在代码当中import的header。完成过后再去以相同的道理执行header模块
最后将header这个模块导出的整体的对象通过require函数return回去
module中的exports应该是一个对象,因为ES Module里面默认导出它是放在default上面。
此时将模块的导出对象拿到,然后访问里面的default。这个时候调用这个default函数,内部还是会调用内部模块的代码
最终将创建完的元素拿到并append到body上面
这实际上就是Webpack打包大致的运行过程。
我们可以看出来Webpack打包过后的代码并不会特别复杂,它只是说帮我们把所有的模块给放到了同一个文件当中。除此之外,它还提供了一些,让我们的模块与模块之间相互依赖的关系还可以保持原来的状态。
Webpack 不仅仅是JavaScript模块化打包工具,它更应该算是整个前端项目或者叫前端工程的模块打包工具,这也就是说我们还可以通过Webpack引入我们在前端项目中的任意类型文件。
接下来我们尝试Webpack打包CSS文件,项目代码
Webpack内部默认只会处理JavaScript文件,我们可以通过适当的Loader
(加载器)来处理CSS文件,内部的Loader只能处理JS文件,我们可以在去为其它类型的资源文件添加不同的Loader。
这里需要的是一个css-loader,我们来安装一下
yarn add css-loader --dev
如果只是使用这样一个Loader,我们并不能得到预期的结果。因为css-loader的作用是将CSS文件转换为一个JS模块。只是将css代码push到这样一个由css-loader内部提供的数组当中,整个过程并没有去用到这个数组。
所以我们还需要安装一个style-loader,它的作用就是把css-loader转换过后的结果通过style标签的形式追加到页面上
yarn add style-loader --dev
安装过后需要在配置文件添加相应的配置
module.exports = {
...
entry: './src/main.css', // 将css文件作为打包入口
...
module: {
rules: [
{
test: /.css$/, // 匹配打包过程遇到的文件路径
use: [ // 指定匹配到的文件使用到loader
// 当我们配置多个loader 执行顺序从下至上
'style-loader',
'css-loader'
]
}
]
}
}
通过以上方法,我们确实可以将CSS文件作为打包入口,不过Webpack打包入口一般还是JavaScript,因为打包入口从某种程度上可以算是应用的运行入口。
就目前而言,前端应用的业务一般是由JavaScript来驱动的,正确的做法还是应该把JS文件作为打包入口,在js代码中通过import的方式来引入CSS文件。
这样的话,css-loader仍然可以正常工作,项目代码
我们先将配置文件的入口改回main.js
module.exports = {
...
entry: './src/main.js',
...
然后在main.js内部通过import导入main.css
// main.js
import './main.css' // 只需要执行
...
我们在为heading.js添加heading.css样式文件并编写简单样式,在heading.js当中导入
// heading.js
import './heading.css'
...
传统的做法当中,我们是将样式和行为分离开单独去维护,单独去引入。而Webpack又建议我们要在js当中去载入css,这到底是问什么呢?
其实Webpack不仅仅建议我们在js中引入css,而是建议我们在编写代码过程当中去引入任何当前代码所需要的文件
是这里的代码想要正常工作,就必须要去加载对应的资源,这也正是Webpack的哲学。
JavaScript代码本身是负责完成整个业务的业务功能,放大来看它就是驱动了整个前端应用。而在实现业务功能的过程当中可能需要用到样式或者图片等等一系列资源文件,如果建立了这种依赖关系
其实学习一个新事物不是说学会它的所有用法你就能提高,因为这些东西照着文档基本上谁都可以。很多时候这些新事物的思想才是突破点,能够搞明白这些新事物为什么这样设计,那你基本上就算是出道了。
目前Webpack社区提供了非常多的资源加载器,接下来我们再来尝试一些有代表性的Loader。
首先是文件资源加载器,项目代码
大多数的资源加载器都类似css-loader,都是将资源模块转化为js代码的实现方式去工作。但是还有一些我们经常用到的资源文件,例如项目当中的图片、字体,这些文件没有办法通过js的方式表示。
对于这一类的资源文件,我们需要用到文件资源加载器,也就是file-loader。
我们在项目当中已经添加了一张普通的图片,我们假设这张图片就是我们再去实现某个功能的时候所需要的一个资源。
按照Webpack的思想,我们也应该在用到这个资源的地方通过import去导入这张图片,然后让Webpack去处理资源的加载
// main.js
import icon from './icon.png'
...
const img = new Image()
img.src = icon
document.body.append(img)
安装文件资源加载器file-loader
yarn add file-loader --dev
打开配置文件,添加一个单独的加载规则配置
module.exports = {
...
output: {
...
publicPath: 'dist/' // 将项目根目录作为网站根目录
},
module: {
rules: [
...
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
除了file-loader这种通过拷贝物理文件的形式去处理文件资源以外,还有一种通过Data URLs的形式去表示文件的方式。
Data URLs 是一种特殊的URL协议,它可以用来直接去表示一个文件。传统的URL一般要求服务器有一个对应的文件,然后通过请求这个地址得到服务器上对应的文件。
而Data URLs 是一种当前url就可以直接去表示文件内容的方式。
也就是说这种URL当中的文本就已经包含了文件的内容,在我们去使用这种URL的时候就不会再去发送任何的http请求。
例如这样