Part2 · 前端工程化实战
模块化开发与规范化标准
文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。共同进步!
上一篇:【前端工程化实战-模块化开发与规范化标准(Day13)】
本篇主要内容是前端模块化开发与规范化标准,接续上一篇
三、常用的模块化打包工具
1. Webpack打包
1.1 模块打包工具的由来及概要
模块化确实很好的解决了在复杂应用开发过程当中的代码组织问题,但是随着引入模块化,Web应用又会产生一些新的问题:
第一个,ESM模块系统,它本身就存在环境兼容问题,尽管现如今主流浏览器的最新版本都已经支持这样一个特性,但是目前还没有办法做到统一所有用户浏览器的使用情况,所以还需要去解决兼容问题;
第二个,通过模块化的方式划分出来的模块文件会比较多,而前端应用又是运行在浏览器当中的,因此每一个在应用当中所需要的文件,都需要从服务器当中请求回来,这些零散的模块文件必将会导致浏览器频繁发出请求,从而影响应用的工作效率;
第三个,在前端应用开发过程当中,不仅仅只有JavaScript的代码需要模块化,随着应用的日益复杂,HTML、CSS等资源文件同样也会面临相同的问题。而且从宏观角度来看的话,这些文件也都可以看作为前端应用当中的一个模块,只不过这些模块的种类和用途跟JavaScript是不同的。
对于整个过程而言模块化肯定是有必要的,不过需要在原有的基础之上去引入更好的方案或者工具去解决上面这样几个问题或者是需求,让开发者在应用的开发阶段,可以继续享受模块化带来的优势,又不必担心模块化对生产环境所产生的一些影响。我们就先对这个所谓的更好的方案或者工具去提出一些设想,我们希望他们能够满足我们的这些设想:
第一点,需要这样一个工具能够编译代码,**开发阶段编写的包含新特性的代码,直接去转换为能够兼容绝大多数环境的代码。**这样一来面临的环境问题也就不存在了;
第二点,**能够将散落的模块文件再次打包到一起,解决了浏览器当中频繁对模块文件发出请求的问题。**至于模块化文件划分,只是在开发阶段需要它,因为它能够更好的代码,但是对于运行环境实际上是没有必要的。所以说可以选择在开发阶段通过模块化的方式去编写,生产阶段还是把它们打包到同一个文件当中。
第三点,需要去支持不同种类的前端资源类型,可以把前端开发过程当中所涉及到的样式,图片,字体等等所有资源文件都当作模块去使用。对于整个前端应用来讲,就有了一个统一的模块化方案。之前介绍的那些模块化方案,实际上只是针对现有JavaScript的模块化方案。现在强调,对于整个前端应用来讲,它的一个模块化的方案。这些资源,有了模块化方案后就可以通过代码去控制,那它就可以与业务代码统一去维护。这样对于整个来讲的话会更加合理一些。
针对前两个需求,完全可以借助于之前所了解过的一些构建系统,去配合一些编译工具就可以实现。但是,对于最后一个需求,很难通过这种方式去解决了,所以说就有了接下来所介绍的一个主题,也就是前端模块打包工具。
前端领域目前有一些工具,就很好的解决了以上这几个问题,其中最为主流的就是Webpack、Parcel、Rollup。以Webpack为例,它的一些核心特性,就很好的满足了上面的那些需求。
-
首先Webpack作为一个模块打包工具(Module Bundler),它本身就可以解决模块化JavaScript代码打包的问题。通过Webpack就可以将零散的模块代码打包到同一个JS文件当中。对于代码中那些有环境兼容问题的代码,可以在打包的过程当中,通过模块加载器(Loader)对其进行编译转换。
-
其次,Webpack还具备代码拆分(Code splitting)的能力,能够将应用当中所有的代码,都按照需要去打包。这样就不会产生把所有的代码全部打包到一起,产生的这个文件会比较大的这样一个问题。可以把应用加载过程当中初次运行的时候所必须的那些模块打包到一起,那对于其他的那些模块再单独存放。等到应用工作过程当中实际需要的某个模块,再去加载这个模块,从而实现增量加载或者叫渐进式加载,这样就不用担心文件太碎或者是文件太大这两个极端的问题。
-
最后对于前端模块类型的问题,**Webpack支持在JavaScript当中以模块化的方式去载入任意类型的资源文件。**例如在Webpack中就可以通过JavaScript去直接import一个css的文件,最终会通过style标签的形式去工作。其他类型的文件,也可以有类似的这种方式去实现。
总之来说,所有的打包工具它们都是以模块化为目标,这里所说的模块化是对整个前端项目的模块化,也就是比之前所说的JavaScript模块化要更为宏观一些。它可以让我们在开发阶段更好的去享受模块化所带来的优势,同时,又不必担心模块化对生产环境所产生的一些影响,那这就是模块化工具的一个作用。
1.2 Webpack快速上手、配置文件
-
Webpack快速上手
Webpack作为目前最主流的前端模块打包器,提供了一整套的前端项目模块化方案,而不仅仅是局限于只对JavaScript的模块化。通过提供的前端模块化方案,我们就可以很轻松的对前端项目开发过程当中,涉及到的所有的资源进行模块化。因为Webpack的想法比较先进,而且他的文档也比较晦涩难懂,所以说在最开始的时候,它显得对开发者不是十分友好。但是随着它版本的迭代,官方的文档也在不断的更新。目前Webpack已经非常受欢迎了,基本上可以说是覆盖了绝大多数现代化的外部应用项目开发过程。
使用***yarn init***初始化项目目录,安装webpack以及webpack-cli,使用yarn webpack,webpack会自动从src中的index.js开始打包(寻找import)
yarn init --yes
yarn add webpack webpack-cli --dev
yarn webpack
运行yarn webpack后,webpack自动将index.js、heading.js打包在dist目录下的main.js中,将index.html中的资源引用变为dist/main.js,并将script中的type="module"取消。再次使用serve .命令运行,发现项目依然可以运行。
如果每次都需要yarn去运行webpack命令,会比较麻烦。可以在package.json中添加scripts字段,并将其设置为“build”:“webpack”,这样就可以直接运行
***yarn build***命令。
-
Webpack配置文件
Webpack4.0以后支持零配置文件打包。也就是说不需要配置文件,直接按照约定的内容区打包,他的约定是默认入口文件为src下的index.js,默认输出为dist下的main.js。我们如果需要按照自定约定去打包,Webpack也支持配置文件打包。
在项目根目录下添加webpack.config.js文件,该文件在node环境下运行,其内容如下:
const path = require('path') module.exports = { entry: './src/main.js', // 打包文件的默认入口,不能省略./ output: { filename: 'bundle.js', // 输出打包文件的文件名称 path: path.join(__dirname, 'output') // 必须为绝对路径,所以需要path模块配合 } }
1.3 Webpack工作模式、打包结果运行原理
Webpack新增了一个工作模式的用法,这种用法大大简化了Webpack配置的复杂程度,可以把它理解成针对于不同环境的基础预设的配置。我们使用yarn webpack打包项目,命令行会出现一个配置警告,大致意思是我们没有设置一个mode的属性,可能会使用默认的production模式去工作,在这个模式下,webpack内部会自动启动一些优化插件,比如自动压缩代码。这对实际生产环境非常友好,但是对于开发环境中,我们无法阅读这些打包结果。
Webpack工作模式:
- production(默认)
- development(开发模式)
- none
可以通过cli参数去指定打包的模式,具体用法就是给webpack传递一个参数:–mode,参数有三种取值,默认就是production,production模式它会自动启动优化,去优化打包结果。第二种参数是development,开发模式会自动优化打包的速度,它会添加一些开发过程中需要的辅助到代码中(后面介绍调试的会详细介绍)。第三种参数是none模式,none模式下,webpack运行最原始状态的打包,不会做任何额外的处理。
yarn webpack --mode production # production为默认的工作模式,可以不用去显式指定
yarn webpack --mode development
yarn webpack --mode none
目前工作模式只有这三种,具体的三种模式的差异可以从官网(https://webpack.js.org/configuration/mode)中找到。当然除了cli参数指定模式,还可以通过配置文件方式指定工作模式。
在webpack.config.js中添加node字段,并指定模式。这样就可以通过yarn webpack直接以配置的方式去打包。
webpack.config.js
const path = require('path')
module.exports = {
// 这个属性有三种取值,分别是 production、development 和 none。
// 1. 生产模式下,Webpack 会自动优化打包结果;
// 2. 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
// 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
mode: 'development',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
我们解读一下webpack打包过后的结果,为了可以更好的理解打包过后的代码,这里先将webpack的工作模式是为none,这样是以最原始的状态去打包代码。
ps:此处使用的版本:
"webpack": "4.40.2",
"webpack-cli": "3.3.9"
webpack.config.js
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
yarn webpack
整个函数是一个立即执行函数,参数为modules。函数调用时传入一个数组,数组中的每个元素是参数列表相同的函数。这里的函数对应的就是源代码中的模块,也就是说每一个模块最终都会被包裹到这样一个函数当中,从而去实现模块的私有作用域。
进入webpack工作入口函数,这个函数内部并不复杂,而且注释也非常清晰。最开始先定义了一个对象,用于存放或者叫缓存加载过的模块。紧接着,定义了一个__webpack_require函数,该函数专门用来加载模块。再往后就是在require函数上面挂载了一些其他的数据和一些工具函数。入口函数执行到最后,调用了require函数,参数传入0,来加载模块。这个地方的模块ID,实际上就是上面的模块数当中的元素下标,也就是说这里才开始去加载在源代码当中所谓的入口模块。
1.4 Webpack资源模块加载
正如一开始提到,webpack并不只是JavaScript的模块化打包工具。它应该是整个前端项目或前端工程的模块打包工具,也就是说可以通过webpack引入任意类型的静态资源文件。接下来通过webpack引入css文件。
首先在项目目录中添加一个main.css文件,内容如下:
body {
margin: 0 auto;
padding: 0 20px;
max-width: 800px;
background: #186fb1;
}
然后回到webpack.config.js下,将入口文件的路径指向新创建的css文件。随后配置loader组件,test值为正则表达式/.css$/,use配置一个数组,分别为style-loader以及style-loader
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
// rules数组针对于其他资源模块的加载规则,每个规则对象的都需要设置两个属性。
rules: [
{
test: /.css$/, // test用来去匹配在打包过程当中所遇到的文件路径
// use用来去指定我们匹配到的文件,需要去使用的loader
use: [
'style-loader',
'css-loader'
]
}
]
}
}
命令行启动,yarn webpack,通过serve . 运行,在浏览器中访问就可以看到我们的css生效了。
ps:use中,如果配置了多个loader,其执行顺序是从数组最后一个元素往前执行。所以这里一定要把css-loader放到最后,因为我们必须要先通过css-loader把css代码转换为模块才可以正常打包。
style-loader工作代码在bundle.js中,部分代码如下:
loader是webpack实现整个前端模块化的核心,通过不同的loader就可以实现加载任何类型的资源。
1.5 Webpack导入资源模块
通过上面的探索,webpack确实可以把css文件作为打包的入口文件。不过webpack的打包入口文件一般是JavaScript。因为打包入口文件从某种程度来说,算是应用的运行入口。前端应用当中的业务是由JavaScript去驱动的。上面只是尝试一下,正确的做法还是将JavaScript文件作为打包的入口文件。
webpack.config.js
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
分别新建main.css以及heading.css文件,代码如图所示。分别在heading.js和main.js中通过import引入css文件。因为heading.css使用了类选择器,所以需要在黄色框中为element添加类名。
构建完成后,命令行运行yarn webpack,随后运行serve .命令,在浏览器打开,发现css文件生效。
传统的做法当中是将样式和行为分离开,单独去维护,单独去引入。而webpack中建议我们在JavaScript中引入css。原因是webpack不仅仅建议我们在JavaScript中引入css,而是建议我们编写代码过程中,去引入任何当前模块所需要的资源文件。因为真正需要资源的不是应用,而是此刻正在编写的代码,他想要工作,就必须加载对应的资源。
通过JavaScript代码去引入资源文件,或者叫建立我们JavaScript和资源文件之间的依赖关系。它有一个很明显的优势,JavaScript代码本身是负责完成整个应用的业务功能,那放大来看,它就是驱动了我们整个前端应用,而在实现业务功能的过程当中,可能需要用到样式、图片等等一系列的资源文件,如果建立了这种依赖关系,一来逻辑上比较合理,因为我们的JavaScript确实需要这些资源文件的配合,才能去实现对应的功能,二来保证上线时资源文件不会缺失,而且每一个上线的文件都是必要的。
1.6 Webpack文件资源加载器
目前webpack社区提供了非常多的资源加载器,基本上能想到的所有合理的需求都会有对应的loader,接下来尝试一个非常有代表性的资源加载器。
大多数的加载器都类似于css-loader,都是将资源模块转换为js代码的实现方式去工作。但是,还有一些经常用到的资源文件,例如项目当中的图片或者字体,这些文件是没有办法通过js的方式去表示的,对于这一类的资源文件,需要用到文件资源加载器,也就是file-loader。
项目src目录中添加一张图片icon.png,在main.js中通过import的方式导入图片,并且创建图片标签,将其src设置为导入的接收值。
初始目录:
main.js
import createHeading from './heading.js'
import './main.css'
import icon from './icon.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = icon
document.body.append(img)
随后在webpack.config.js中设置一个新的规则,当遇到.png结尾的文件时,使用file-loader加载器。
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/' // 网站的根目录
},
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'file-loader'
}
]
}
}
安装file-loader,命令行使用yarn webpack打包资源。serve .运行,通过浏览器可以观察到,img资源被加载成功。
打包后的目录:
总结文件加载器的工作过程,webpack打包过程中遇到图片文件,根据webpack.config.js中的配置规则对应到file-loader加载器。file-loader首先将导入的(图片)文件复制到输出目录dist,然后将文件拷贝到输出目录的路径作为这个模块的返回值,对于这个应用来说,这个资源就被发布了。同时,也可以通过模块的导出成员拿到这个资源的访问路径。