作者:supot
原文:https://juejin.cn/post/6962902504212267021
在ES6之前,JavaScript没有一个标准的模块方案,社区比较流行的是AMD方案和node使用的CommonJS的方案。
为了能够在浏览器端使用npm上大量的CommonJS规范的包,需要我们去对模块进行兼容和处理,这就是前端打包需要解决的一个问题。
Webpack是现在主流的打包工具,不管是蚂蚁的umi还是vue的vue-cli,底层都是基于Webpack来进行二次封装的。
Webpack 官方将它定位为一个 module bundler
,它通过定义的入口文件,进行依赖收集,构建出项目的依赖图,最后生成并输出一个或多个bundle。
下图是官方给出的Webapck对于模块的处理流程
我们希望浏览器能够顺利的运行第三方的包和业务代码,不管是commonjs、requirejs或者是es6的模块,所以需要提供一个新的模块系统,将这些第三方包和项目里的业务模块进行统一处理,以便在浏览器中能正确的运行。
webpack 用类似于node 的commonjs的模块方案,将最终打包的代码运行在浏览器中。
(function(modules) { // webpackBootstrap
// 已经加载的模板
var installedModules = {};
// 模块加载函数
function __webpack_require__(moduleId) {
// ...
}
// 入口文件
return __webpack_require__(__webpack_require__.s = "./index.js");
})
({
"./index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
"./utils/test.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
复制代码
上面就是webpack打包出来的代码,其中index.js是项目的入口文件,所以的模块都通过 __webpack_require__
进行加载,并且会把加载完成的模块进行缓存。
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId, // Module ID
l: false, //
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
通过__webpack_require_
的代码可以看出,webpack实现了一个类似CommonJS的模块方案。
对于webpack来说,如果仅仅解决不同方案模块的聚合和运行,是远远不够的。对于越来越复杂的前端项目,需要提供更加底层的能力给开发者,去实现不同业务场景的不同需求。
Webpack 基于 Tapable 实现了一个复杂的插件系统,在它的构建过程中,对外抛出了各个关键的节点的hook,开发者可以通过订阅这些勾子,对webpack中的资源进行加工和处理,从而完成定制化的需求
通过Plugin和Loader,能够不断扩展Webpack的功能。
当然,Webpack 也不是十全十美的,在学习和试用过程中,也会存在大大小小的问题
概念和配置项太多,需要配合实际的项目场景不断的优化配置文件,umi、vue-cli等脚手架就是为了解决无法开箱即用的问题
随着项目的不断变大,devServer的启动时间会越来越长,并且hmr的速度也会受到影响
功能缺失,在webpack5之前,没有系统级别的文件缓存系统
由于http/2的普及,使得很多基于http/1的优化工作都变成了反模式,其中最主要的一条就是合并代码减少网络请求。
因为在http/1的时代,浏览器只能并行的发起5个请求,这就造成如果文件太多,会存在请求阻塞的问题,所以在很多项目中,我们都会去对代码进行打包生成尽量少的vendor。但是在http/2里,所有的请求都会在一个tcp链接里面完成,并且资源都是并行加载的,这时候单个大文件的加载时间反而会比多个小文件的时间要多。所以在http/2的网络里,我们需要对项目资源进行合理的拆分,充分利用资源的并行请求来减少资源的加载时间。
在es6中,加入了JavaScript的模块。
只需要在script标签上加上 type=module
的属性,浏览器就会将内联代码或者外部引用的脚本视为ECMAScript模块。
相比于 Node 的 CommonJS 模块,ES Module 有很多的不同:
ES module 抛出的是一个引用,而exports抛出的是一个值
require 可以进行动态引用,而ES module需要在作用域的顶层声明所有的依赖,这就导致Node是可以在运行时去加载模块的, 而ES module可以在编译阶段就完成所有的依赖分析,并为后面的优化做准备(比如tree shaking)
...
下面的代码可以在浏览器中直接运行
// test.js
export default function hello() {
console.log('hello world');
}
// index.html