文中使用版本:webpack4.x
第一章:webpack简介
模块化思想:
按照特定的功能将其拆分为多个代码段,每个代码段实现一个特定的目标。你可以对其进行独立的设计、开发和测试,最终通过接口来将它们组合在一起。
模块化解决了哪些问题?
- 依赖关系:通过导入导出语句
- 减少服务器请求,网络开销:资源合并
- 作用域污染:模块间作用域隔离
ES6模块标准应用阻力:
- 无法code splitting(代码分割)和tree shaking(抖掉未使用代码)
- npm模块大多CommonJS形式,浏览器并不支持,因此无法直接拿来用
- 个别浏览器及平台兼容问题
因此,在使用模块化同时兼容浏览器,需要模块打包工具
对比同类打包工具,webpack优势:
- 支持多种模块标准(AMD、CommonJS、ES6 module)
- code splitting(代码分割)
- 处理多类型资源(使用loader)
- 社区支持
Tips:
webpack建议本地安装,避免了多个开发者之间版本不一致导致的问题
本地的webpack无法命令行直接使用“webpack”指令,工程内部只能使用npx webpack 的形式,后续简化
安装:
npm install webpack webpack-cli --save-dev
为什么要安装webpack-cli?
这里是引用
默认配置及文件:
- 入口src/index.js
- 出口dist/
- 配置文件webpack.config.js
本地开发工具webpack-dev-server
Tips:
指令npm install 和 npm install --production区别
- npm install: 会下载全部devDependencies 和 dependencies包
- npm install --production: 只下载dependencies包
webpack-dev-server优势:
- 令webpack打包:
它并不会做其他额外操作,所有webpack所需配置都要自己配置,包括所承载页面的index.html页面也需要自己加到项目
- 作为web Server
起一个普通的服务器,让项目可以在浏览器地址栏通过http://localhost:port访问
打包结果放在内存中,不存在于项目dist文件夹中
webpack-dev-server还有个特性live-reload(自动刷新)
- live-reload: 自动刷新页面,全部资源会重新下载一遍
- hot-module-replace(模块热替换):不刷新页面,只局部更新,不会再下载其他无关资源(看起来更好?)
第二章:模块打包
CommonJS:
导出:
// 🌈正确示例:
module.exports = {
name:'calculator',
add: function(a, b){return a + b;}
}
// 等同于
exports.name = 'calculator';
exports.add = function(a, b){return a + b;};
// ❌错误示例
exports = {
name:'calculator',
add: function(a, b){return a + b;}
}
// 原因:默认会添加
// var module = {
// exports: {}
// }
// var exports = module.exports
// 如果对exports重新定义为对象,会自动解除exports和module间的关联关系
不要将module.exports与exports混用,因为有一个会被冲掉
在导入一个模块时,整个文件都会被调用一遍,虽然收到的模块对象是module.exports或exports出来的对象,但之后的代码也会被执行一遍
module.exports= {
name: 'calculator'
}
console.log('end')
require该模块后,console语句也会输出,但我们一般不推荐这么用,而是将它提前
导入:
使用const modA = require('moduleA')
方式导入模块,如果只需要执行不需要导出对象(例如初始化操作),可以不使用const modA接收对象。
如上例,console.log(‘end’)会在第一次导入时输出,再次导入时不再输出,而是直接使用上次module.exports或exports的结果对象(值拷贝?)
require函数可接受表达式,可以利用这个特性进行动态加载模块
['modA','modB'].forEach(name => {
require('./' + name)
})
ES6 Module:
// calculator.js
export default {
name:'calculator',
add: function(a, b){return a + b;}
}
// index.js
import calculator from './calculator.js'
const sum = calculator.add(2, 3)
console.log(sum) // 5
- export import 是ES6保留字,
- CommonJS中module不是保留字,可以被重新定义使用
ES6 Module中默认开启严格模式,不论有没有在顶部添加"use strict"字样,所以ES5转ES6的代码时要注意
导出:
- 命名导出
// 写法1:
export const name = ‘calculator’;
// 写法2:
const name = ‘calculator’;
export {name}
// 也可以export {name as 别名}
- 默认导出
// 整个js文件模块只能有一个default导出
export default {
name: ‘calculator’
}
// 或者
export default ‘calculator’
导入:
- 🚩对于命名导出
export {
name: 'calculator',
add: function(a, b){
return a + b
}
}
导入时可以ES6解构
导入(名称必须在export中存在,一一对应):
import {name, add} from './calculator.js'
// 或者使用别名(如有的变量名已经被使用) import {name, add as addSum} from './calculator.js'
add(3, 4);
// 若使用别名,则文中add方法并不存在,使用别名进行调用 如:addSum(3, 4)
也可以整体导入(减少对当前作用域的影响)
import * as 别名 from './calculator.js'
别名.add(3, 4);
- 🚩对于默认导出
export default {
name: 'calculator',
add: function(a, b){
return a + b
}
}
导入时可以
import 别名 from './calculator.js'
别名.add(3, 4)
可以理解为
import {default as 别名} from './calculator.js'
别名.add(3, 4)
- 🚩混合导入(命名导入+默认导入)
导出:
// react.js
export Component {
name: 'comp1'
fn: function(){}
}
export default {
// code
}
导入:
import React, {Component} from 'react'
Tips: 默认导入必须放最前,否则语法报错
复合导出
- 对于
命名导出
,进行导入再导出
// a.js
import {name, add} from './calculator.js'
export {name, add}
可以合并为一行
// a.js
export {name, add} from './calculator.js'
- 对于
默认导出
,则不能进行复合,可以理解为没有依附的变量名
import calculator from './calculator.js'
export default calculator
CommonJS 与 ES6 Module对比
对比角度 | cjs | esm |
---|---|---|
建立模块依 赖 | 动态模块依赖,可以动态require(’./’ + modName),模块依赖关系运行时 确定 | 静态模块依赖,模块依赖关系编译时 确定(一般理解为webpack打包过程中,那时bundle代码还未在服务器中运行) |
模块导入 | 值拷贝 | 变量映射 |
esm静态优势:
- 打包时确定无用代码,减小体积
- 模块变量类型检查
- 编译器优化1
值拷贝(cjs) 与 动态映射(esm)
值拷贝:只第一次获取值并存储,方法内不可改变值,外部可随意更改值
动态映射:每次获取值,值映射可方法内部改变值,外部不可更改
循环依赖
- cjs对于循环依赖的处理,在依赖过程中先导出module.exports这空对象
如上文所说,module.exports是在模块顶部默认创建了module对象并添加属性exports:{},所以在代码未结束时就导出了该module.exports空对象,然后逐步继续执行。 - esm输出的是undefined
esm利用动态映射的特性,可以对循环依赖进行控制,达到预期目的
做法:导出function实现延后执行,在function中通过开关标识进行阻断,第一次之后就进行切换标识状态,防止循环调用。
结论: ES6 Module可以更好地支持循环依赖
非模块化文件:
对于对象绑定全局的,直接import即可
import './jquery.min.js'
对于隐式绑定全局的,进行webpack打包后不会自动绑定到全局,因为外面加了一层function
// 在原生代码中,变量在最外层声明是会自动绑定到windows对象上的
// 但在webpack处理后,文件内容外加了一层,变成了隔离作用域,并不会自动加到全局对象上了
var calculator = {
name: 'calculator',
add: function(){}
}
AMD:
优点:异步不阻塞
// 定义
define(模块名, [依赖模块], function(依赖模块1,依赖模块2,...) {
return something
})
// 调用
// 遇到require方法不等待,继续执行otherFn()
require([模块], function(模块){
// code
})
otherFn()
缺点:
- 语法冗长
- 异步加载的方式不如同步显得清晰
- 易造成回调地狱
UMD:
通用模块标准,目标:使一个模块能兼容各模块环境,
注意判断顺序,一般AMD -> CommonJS -> 非模块化
而在webpack中两者都支持,使用AMD优先则不符合预期,故应该更换顺序为CommonJS -> AMD -> 非模块化
// 一般umd写法
(function (global, main) {
if (typeof define === 'function' && define.amd) {
define(...)
} else if (typeof exports === 'object') {
module.exports = ...
} else {
global.add = ...
}
}(this, function(){
return {...}
}))
模块打包原理:
函数体中:
(function(modules) {
// 这里有个installedModules对象为每个已经请求过的模块作缓存
function __webpack_require__(moduleId) {
...
}
return __webpack_require__(__webpack_require__.s = 0)
})({
// 将各个模块添到对象,需要时调用\_\_webpack\_require\_\_方法,参数为模块名0、''3qiv"、"jkzz"
0: function(){...},
'3qiv': function(){...},
jkzz: function(){...},
...
})
这是一个赋值并立即执行的匿名函数,是已经构建好的完整函数
依赖的模块方法怎么获取的,这里并未说明,后文有提到
第三章:资源输入输出
资源处理流程:
从entry入口作为根,逐级检索依赖,形成依赖树,
如果多个entry,就会有多棵树
资源入口:
单入口:
文件字符串:chunk name默认为main
对象形式指定[chunk name]:chunk name为指定的[chunk name]
多入口:
对象形式指定[chunk name]:chunk name为指定的[chunk name]
context:非必填,资源入口的路径前缀,就为了其他配置写地址时候可以省略前面路径的一大堆,默认项目跟目录
entry:必填,可以是字符串、数组、对象、函数。
- 数组中,最后一个元素为实际入口路径,常见的有
module.exports = {
entry: ['babel-polyfill', './src/index.js']
}
等效于 在./src/index.js中 先引入了babel-polyfill
模块
- 对象中,属性也可以是字符串或
数组
- 函数中,可以动态获取入口文件,返回一个对象,也可以返回一个Promise对象
cjs: require引入时查找整个对象,且值拷贝;
esm: import引入时以变量方式,减少引用层级 ↩︎