浏览器端的模块化
浏览器端存在的问题?
- 效率问题,模块划分带来了更多的js文件,更多的js文件带来了更多的请求,降低了页面的访问效率
- 兼容性问题,目前浏览器只支持ES6模块化标准,并且还存在兼容性问题
- 工具问题,浏览器不支持npm下载的第三方包
- es6标准中,import时的路径必须以./或者../开头
- 第三方模块使用其他模块化标准
所以浏览器端很难与npm结合
node端是否存在这些问题?
不存在,因为:
- node端运行的js文件在本地,node可以本地读取文件,效率比浏览器远程传输文件高得多
- node端支持es6模块化
- node端支持npm下载的包(require(路径不以./或者../开头),则会去查找node内置模块或者node_modules中的依赖包)
并且在浏览器端,开发时态和运行时态的侧重点不一样(根本原因)
开发时态:
- 模块划分越细越好
- 支持多种模块化标准
- 支持npm或其他包管理器下载的模块
- 能够解决其他工程化的问题
运行时态:
- 文件越少越好
- 文件体积越小越好
- 代码内容越乱越好
- 所有浏览器都要兼容
- 能够解决其他运行时的问题,主要是执行效率问题
如何解决浏览器端的这些问题?
需要一个工具,利用工具把开发时编写的代码转换为运行时需要的东西,这样的工具,就叫做构建工具
常见的构建工具:
- webpack
- vite
- grunt
- gulp
- ....
webpack中的模块化兼容性
问题:webpack同时支持commonjs和es6 module,不同的模块化可交互,在互操作时webpack是如何处理的?
1、同模块化标准
如果导入导出使用的是同一种模块化标准,打包后的结果和模块化的没有任何差异
2、不同模块化标准
- es6导出,commonjs导入
// a.js( es6导出)
export let a=1
export let b=2
export default 3
// index.js(commonjs导入)
const a = require('./a')
console.log('a',a)
结果:
- commonjs导出,es6导入
// a.js(commonjs导出)
module.exports={
a:1,
b:2,
c:3
}
// index.js (es6导入)
import a from './a'
import * as amodule from './a'
console.log('a=>',a)
console.log('amodule=>',amodule)
结果:
注意:
es默认导出,commonjs导入
// a.js
export default{
a:1,
b:2,
c:3
}
//index.js
const a = require('./a')
console.log('a=>',a)
结果:
最佳实践:选择一个合适的模块化标准,然后贯彻整个开发阶段
编译结果分析
//a.js
console.log('a module')
module.exports = 'a'
//index.js
console.log('index module')
const a = require('./a')
console.log('a=>',a)
//自行实现打包后的结果
//myMain.js
(function(modules){
//模块缓存对象
const cachedModule={}
// moduleId 模块路径
function __webpack_require(moduleId){
// 判断模块是否缓存
if(cachedModule[moduleId]){
return cachedModule[moduleId]
}
const func = modules[moduleId] //得到该模块对应的函数
const module={exports:{}}
//执行该模块对应的函数
func(module,module.exports,__webpack_require)
//缓存模块
cachedModule[moduleId] = module.exports
//返回模块导出结果
return module.exports
}
// 执行入口模块
__webpack_require('./src/index.js') //运行一个模块,得到模块导出结果
})({
//该对象包含了所有模块和模块对应代码
'./src/a.js':function(module,exports,__webpack_require){
console.log('a module')
module.exports = 'a'
},
'./src/index.js':function(module,exports,__webpack_require){
console.log('index module')
const a = __webpack_require('./src/a.js')
console.log('a=>',a)
}
})
实现效果:
npx webpack --mode development打包结果:
(() => {
// //该对象包含了所有模块和模块对应代码
var __webpack_modules__ = ({
"./src/a.js":
((module) => {
eval("console.log('a module')\r\nmodule.exports = 'a'\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/index.js":
((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("console.log('index module')\r\n\r\nconst a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\n\r\nconsole.log('a=>',a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
// 模块缓存存储对象
var __webpack_module_cache__ = {};
// 定义__webpack_require__函数
function __webpack_require__(moduleId) {
// 查找缓存
var cachedModule = __webpack_module_cache__[moduleId];
//有缓存,直接返回缓存结果
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 没有缓存,往缓存对象中添加
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
//执行该模块对应的函数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回该模块导出结果
return module.exports;
}
// 执行入口文件
var __webpack_exports__ = __webpack_require__("./src/index.js");
})()
;
编译过程
webpack的作用:是将源代码(构建、打包)成最终代码
整个过程大致分为
- 初始化
- 编译
- 输入
初始化
webpack会将cli参数,配置文件,默认配置进行融合,形成一个最终的配置对象
编译
1、创建chunk
chunk是webpack在内部构建过程中的一个概念,译为块,他表示通过某一个入口找到的所有依赖的统称
根据入口模块,创建一个chunk,每个chunk都有至少两个属性:
- name:默认为main
- id:唯一编号,开发环境下和name相同,生产环境下是一个数字,从0开始
2、构建所有依赖模块
检查记录:在模块记录中存在,则直接返回模块id对应的转换后的代码,不加载该文件
读取文件内容:只读取,不执行
替换依赖函数:把导入语句替换为 __webpack_require__函数,并把函数的路径参数替换为moduleId
3、产生chunk assets
在第二步骤完成后,chunk中会产生一个模块列表,该列表中包含了模块id和模块转换后的代码
接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容
chunk hash:根据所有chunk assets的内容生成一个hash字符串
4、合并chunk assets
将多个chunk的assets合并到一起,并产生一个总的hash
输出(emit)
总结
开启watch后,每次文件变化都会从编译开始,重新编译重新输出。
loader
本质上是一个函数,在打包过程中,将一个源码字符串转为另一个源码字符串返回。
loader的工作时机
在读取文件内容之后
处理loader的流程
loader配置
// myLoader.js
function myLoader(sourceCode){
return sourceCode.replace(/abc/g,'let')
}
module.exports = myLoader
module.exports={
mode:'development',
module:{
rules:[
{
test:/index\.js$/,
use:['./loaders/myLoader']
}
]
}
}
//index.js
abc name='hello'
编译后的结果:
// main.js
eval("let name='hello'\n\n//# sourceURL=webpack:///./src/index.js?");
loader的本质是一个函数,函数参数是读取的文件内容或者上一个loader返回的内容,当检测到当前模块满足rules的某个规则,就会把该规则对应的loader放入loaders数组中,之后按照一定的顺序执行loader
loader的执行顺序
module.exports={
mode:'development',
module:{
rules:[
{
test:/index\.js$/,
use:[ './loaders/loader1', './loaders/loader2', ]
},
{
test:/\.js$/,
use:[ './loaders/loader3', './loaders/loader4', ]
}
]
}
}
可以理解为在匹配模块规则时,先定义一个空数组loaders,模块规则全部匹配失败,则返回这个空数组。模块规则匹配成功,则把该规则下的use中的loader都push到这个loaders,最后所有规则匹配结束,将loaders中的loader从尾部开始取出进行处理(loaders先进后出,可以看成是一个栈结构)。
loader的配置参数options
// webpack.config.js
module.exports={
mode:'development',
module:{
rules:[
{
test:/index\.js$/,
use:[
{
loader:'./loaders/myLoader',
options:{
replaceChar:'abc'
}
},
]
}
]
}
}
//myLoader.js
function myLoader(sourceCode){
// 通过this.query获取options中的数据
const {replaceChar} = this.query
const reg = new RegExp(replaceChar,'g')
return sourceCode.replace(reg,'let')
}
module.exports = myLoader
还可以直接将参数以query的形式写到路径之后: ./loaders/myLoader?replaceChar=abc&a=1',此时的this.query的值是:?replaceChar=abc&a=1
loader示例
loader处理css文件
// cssLoader.js
module.exports=function(sourceCode){
const data = `const style = document.createElement('style')
style.innerHTML = \`${sourceCode}\`
document.head.appendChild(style)`
return data
}
loader处理图片
根据参数,判断图片是转成base64的形式还是输出一张图片
// webpack.config.js
module:{
rules:[
{
test:/\.(png)|(jpg)|(jpeg)$/,
use:[{
loader:'./loaders/pictureLoader',
options:{
limit:10000,
filename:"img-[contenthash:5].[exrt]"
}
}]
}
]
}
//pictureLoader.js
const loaderUtils = require('loader-utils')
const pictureLoader = function(buffer){
const {limit,filename} = this.query
const size = buffer.byteLength
let concent;
if(size<limit){
concent = getBase64(buffer)
}else{
concent =getFilePath.call(this,buffer)
}
return `module.exports=\`${concent}\``
}
// 该loader要处理的是原始数据
pictureLoader.raw = true
module.exports=pictureLoader
// 图片转base64
function getBase64(buffer){
return "data:image/png;base64,"+ buffer.toString('base64');
}
function getFilePath(buffer){
const filename = loaderUtils.interpolateName(this,"[contenthash:5].[ext]",{
content:buffer
})
// 生成文件
this.emitFile(filename,buffer)
return filename
}
plugin
plugin概念
作用:在webpack的某些编译节点添加监听事件,去做一些处理,当XXX时,XXX样,把某些功能嵌入到webpack的编译流程中
本质上是一个有apply方法的对象,通常会将该对象写成构造函数的模式
class MyPlugin{
apply(compiler){
}
}
compiler对象是在初始化阶段构建的,整个webpack打包期间只有一个compiler对象,后续完成打包工作的是compiler对象内部创建的compilation
apply方法会在创建好compier对象后调用,并向方法传入一个compiler对象
complier对象提供了大量的钩子函数,plugin中可以注册这次钩子函数,参与webpack的编译和生成
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name,function(complilation){
//事件处理函数
})
}
}
事件名称可在官网API的Plugins中查看,事件类型可在官网找到Tapable文档进行查看
plugin配置
// webpack.config.js
//引入plugin
const MyPlugin = require('./plugins/MyPlugin)
module.exports={
plugins:[
new MyPlugin()
]
}
plugin示例
FilelistPlugin:将编译输出的文件的文件名称,文件大小写入filelist.text文件中,并一起输出
class FileListPlugin{
apply(compiler){
compiler.hooks.emit.tap('FileListPlugin',compilation=>{
const fileNames= Object.keys(compilation.assets)
const contentList=[]
for(let key of fileNames){
let current = ''
current+=`【${key}】
大小:${compilation.assets[key].size()/1000}KB`
contentList.push(current)
}
const str = contentList.join('\n\n')
compilation.assets['filelist.text'] = {
source(){
return str
},
size(){
return str.length
}
}
} )
}
}
module.exports=FileListPlugin