重学webpack系列(二) -- webpack解决的问题与实现模块化的具体实践
上一章我们一起回顾了前段模块化的发展历程,详情请戳 >>> 重学webpack系列(一) -- 前端模块化的演变历史,只是根据几个想法,我们便创造出了webpack打包工具,它能够根据我们在前端项目中遇到的疑难杂症对症下药,那么这一章我们就一起来探讨一下我们项目落地所遇到的种种问题。
前端实践中的问题
- Jsx / Tsx编译问题
- Less / Scss编译问题
- TypeScript编译问题
- 语法 / 环境兼容问题
- 代码压缩问题
- 不同类型的静态资源处理问题
- 模块化文件的统一管理问题
- .....
上面的这些问题,都是在开发中遇到的实际问题,每当启动一个项目的时候,我们首先需要去手动tsc、手动编译jsx,手动去编译.less文件等等,初始化的时候我们需要走一遍这样的编译流程,如果文件中出现一丁点修改的话,那将必定会再走一遍编译流程,前端开发苦不堪言,基于此webpack做的事情,就是把这一套流程所需要的工具,全部集成起来,让前端开发者只用关注于自己的业务代码,而不用操心其是如何编译的。那webpack是怎么去解决这些问题的呢?
webpack可以通过加载多种loaders来解决编译的问题。webpack本身机制可以实现将多个js文件打包成一个或者按需打包成多个文件,实现文件的统一管理问题。webpack可以通过各种plugins来解决代码压缩,分割等问题。- 如果是
html页面所使用到的文件,webpack也能够提供像原生一样的方式进行模块的加载。
我们知道了这么多的概念与做法,那webpack具体是怎么实现的呢?
webpack实现模块化的具体实践
我们应当利用webpack去初始化一个项目,npm init -y ; npm i webpack webpack-cli -S,我们就会生成如下的package.json。
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}
webpack.config.json
const path = require('path')
module.exports = {
// 模式
mode: 'development',
// 入口
entry: {
// 单入口,或者用数组的形式
index: './src/index.js'
},
// 出口
output: {
//打包的目录文件夹
path: path.resolve(__dirname,'build'),
//打包的目录文件名
filename: '[name].js',
//打包的公共路径,确保在当前域名下可以正常访问
publicPath: '/'
},
// 打包输出源代码,以便于更好的追踪错误
devtool: "source-map",
}
这里webpack5与webpack4,打包的的信息是不一样的,比如以下:
webpack4

webpack5

entry
在webpack.config.json里面,我们看到了entry配置,这个就是配置可以有三种方式:字符串,数组,对象
// 配置为数组输出main.js
entry: ['./src/index.js', './src/add.js']
// 配置为对象输出以key命名的的文件
entry: {
index:"./src/index.js",
add:"./src/add.js"
}
// 配置为字符串输出main.js
entry: './src/index.js',
多入口打包有什么用呢?多入口打包也算作性能优化的一种吧,他能够帮我们把一个文件所依赖的模块进行另外的输出,从而在某个层面减少了加载一个大文件的网络耗时。
output
在webpack.config.json里面,我们看到了output配置,下面我们来讲解一下其中的属性。
output: {
//打包的目录文件夹
path: path.resolve(__dirname,'build'),
//打包的目录文件名
filename: '[name].js',
//打包的公共路径,确保在当前域名下可以正常访问
publicPath: '/'
},
output是一个对象,其中path指定的输出文件目录位置,filename为打生成的文件的名字,我们可以在文件名字后面加上hash值,这样做的好处有很多,比如利用缓存优化,浏览器网络请求优化等,具体如下:
output: {
//打包的目录文件夹
path: path.resolve(__dirname,'build'),
//打包的目录文件名
filename: '[name].[hash].js',
//打包的公共路径,确保在当前域名下可以正常访问
publicPath: '/'
}
上述代码我们可以实践一下(index.js依赖了add.js)
第一次打包的文件hash

只修改了index.js之后打包的文件hash

只修改了add.js之后打包的文件hash

可见对于hash来讲,只要项目里面的内容改变了,就会生成新的hash,如果不想因为index.js修改影响到了整个文件的hash值,那么这里就要使用chunkhash了,还有一种contenthash用来描述文件内容的hash值,适用于js文件中引入css文件,一般的当js文件改变,就会重新构建js与引入的css文件,contenthash就可以避免在js文件改变的同时,会去重新构建css的情况。
publicPath属性的作用在于提供一个公共路径,能够保证在服务器上JavaScript能够正确的访问到资源所在的位置。
mode
webpack对于mode,提供了三种基本内置优化预设。
development:自动优化打包速度,启用内置插件更好的捕捉错误,便于代码调试。production:启动内置插件优化打包结果,不过打包速度偏慢,便于生产模式访问效率none:基本上不使用
关于上述mode的实际配置,参考webpack官网介绍的mode 。通过配置mode、entry、output我们就可以实现一个基本的模块化构建了。
webpack打包的结果分析
我们以build/add.xxx.js为例简单的分析一下,在打包(webpack5.x打包版本)的源文件中,我们可以看到他的外层其实就是一个自执行函数。
在函数_webpack_require_上面挂载了.r、.d、.o的方法以便于我们形成:加载模块->处理模块->导出模块的链路,具体内容是下面这样的:

稍微调试一下
我们可以看到在_webpack_require_中,这个moduleId为依赖的模块路径,最后把依赖的模块,通过module的exports属性当做对象暴露出去。其中__webpack_module_cache__为缓存内容,可以在二次调用的时候直接读取,不用重新构建生成。

那_webpack_require_.d、_webpack_require_.r、_webpack_require_.o到底是怎么实现的呢?这些方法由自执行函数包裹起来,以便于每一次执行脚本自动挂到主函数_webpack_require_上面去。
// __webpack_require__.d
(() => {
__webpack_require__.d = (exports, definition) => {
// 如果definition有key而exports中没有
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
// 给exports上添加key属性,value为definition[key]
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
// __webpack_require__.o
(() => {
// 用来检测key是不是definition得一个私有属性
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
// __webpack_require__.r
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
// 如果兼容Symbol
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// 给exports添加上{[Symbol.toStringTag]:'Module'}
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 给exports添加上{'__esModule':true}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
这里的_esModule是一个什么东西呢?__esModule是用来兼容ESM模块导入CommonJS模块默认导出方案,但是在vscode编辑器里面单独去执行文件的话,这种方案还是不可以的,可见webpack之强大。

总结
其实webpack的使用并不复杂,在webpack4以后很多的配置都被简化掉了,还是那句话,作为一个高级前端开发工程师,我们不仅要关注表层的东西,我们还必须要去深入理解底层机制与原理。很多同学觉得难大部分原因是因为没有那种耐心去过多的关注枯燥而又乏味的东西吧。如果同学们也有这种探索底层原理或者更深层次的东西的想法,欢迎大家点赞、关注,跟我一起来学习吧。webpack强大是因为他集成了超多的工具来一起实现前端项目落地,下一章我们将会来探索一下webpack的loaders系统,直通车 >>> 重学webpack系列(三) -- webpack的loader机制的解读
转载自:https://juejin.cn/post/7145726110914281509