webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具
前端开发中遇到的问题:
- 需要通过模块化的方式来开发
- 使用高级特性,加快开发效率或者安全性,比如 es6+,ts开发脚本逻辑,sass、less编写样式代码
- 监听文件的变化,并且反映到浏览器上,提高开发效率
- 需要将代码进行压缩,合并以及其他相关的优化
打包工具需要具备的能力:(以实现模块化为目标)
- 能后将散落的模块打包到一起
- 能够编译代码中的新特性
- 能够支持不同种类的前端资源模块
webpack 不同环境的预设配置
- production
启动内置优化插件,自动优化打包结果,打包速度偏慢 - development
自动优化打包速度,添加一些调试过程中的辅助插件,以便于更好的调试错误 - none 模式
运行最原始的打包,不做任何额外的处理,一般在分析模块的打包结果时用到
Loader
使webpack管理项目中任意类型的资源文件,所有资源的加载都是JS代码控制
比如css-loader使用方式
css-loader 只会把css模块加载到js代码中,而并不使用这个模块,所以一般配合style-loader使用
实现一个markdown-loader
实现逻辑:
1、安装一个能将markdown解析为html的模块–marked
2、在markdown-loader.js 中导入这个模块
3、用这个模块解析source
//markdown-loader.js
const {marked} = require('marked')
module.exports = source => {
//将markdown 转为html字符串
const html = marked(source)
// 将html字符串,拼接为一段导出字符串的js代码
const code = `module.exports= ${JSON.stringify(html)}`
return code
}
//webpack.config.js
{
test:/\.md$/, //正则表达式,匹配文件类型
use:['html-loader','./markdown-loader'] //申明使用什么loader进行处理
}
plugin
webpack 插件机制的目的,是为了增强webpack在项目自动化构建方面的能力
插件常见的场景:
- 实现自动在打包之前清除dist目录
- 自动生成应用所需要的Html文件
- 根据不同环境为代码注入类似API地址这种可能变化的部分
- 拷贝不需要参与打包的资源文件到目录
- 压缩webpack打包完成后输出的文件
- 自动发布打包结果到服务器实现自动部署
插件实现机制
plugins是可以用自身原型方法apply来实例化的对象。apply只在安装插件被Webpack compiler执行一次。apply方法传入一个webpck compiler的引用,来访问编译器回调。
手写插件 remove-comments-plugin 删除注释
class RemoveCommentsPlugin {
apply (compiler) { // 这个方法会在启动的时候被调用
// compiler 包含此次构建的所有配置信息
// compiler 对象的hooks属性访问到emit钩子,这个钩子会在webpack即将向输出目录输出文件时执行
// 再通过tap方法注册一个钩子函数,tap 方法接受两个参数。插件名称 和 要挂载到这个钩子上的函数
compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
// compilation可以理解为此次打包的上下文
for(const name in compilation.assets) {
// console.log(name) // 输出文件名称
// console.log(compilation.assets[name].source()) // 输出文件名称
if (name.endsWith('.js')) { // js文件
const contents = compilation.assets[name].source()
const noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size:() => noComments.length
}
}
}
})
console.log('RemoveCommentsPlugin')
}
}
module.exports=RemoveCommentsPlugin;
webpack Plugin 的工作原理
- 读取配置的过程中会先执行 new RemoveCommentsPlugin(options) 初始化一个RemoveCommentsPlugin 获得其实例。
- 初始化 compiler 对象后调用 RemoveCommentsPlugin.apply(compiler) 给插件实例传入compiler 对象。
- 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。
webpack 工作原理
根据配置,找到其中的一个文件作为指定的入口,一般为js文件;
根据代码中出现的import或者是require之类的语句解析推断出来这个文件所依赖的一些资源模块,然后再去分别解析每个资源模块的依赖,最终行成了整个项目中所有用到的文件之间的一个依赖关系树
当产生这个依赖关系树之后,webpack会递归这个依赖树,然后去找到每一个节点所对应的资源文件,然后根据配置选项中的loader配置,交给对应的loader去加载这个模块,最后将加载结果放入打包结果之中
webpack执行流程
- 初始化Compiler: webpack(config) 得到Compiler 对象
- 开始编译:调用Compiler 对象 run 方法开始执行编译
- 确定入口:根据配置中的entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的loader 对模块进行编译,再找出该模块所依赖的模块,递归知道所有的模块被加载进来
- 完成模块编译:在使用loader 编译完所有的模块后,得到了每个模块被编译后的最终内容以及他们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk。再把每个chunk准换成一个单独的文件加入到输出列表(这里是可以修改输出内容的最后机会)
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
实现webpack
babel: https://www.babeljs.cn/docs/
使用babel解析ast 结构:
const fs = require('fs')
const babelParser = require('@babel/parser')
const traverse = require('@babel/traverse').default
function webpack(config) {
return new Compiler(config)
}
class Compiler {
constructor (options ={}) {
this.options = options
}
// 启动webpack打包
run () {
// 1.读取入口文件内容
// 入口文件路劲
const filePath = this.options.entry
const file = fs.readFileSync(filePath, 'utf-8')
// 2. 将其解析成ast 抽象语法树
const ast = babelParser.parse(file, {
sourceType: 'module' // 解析文件的模块化方案是 ES Module
})
console.log(ast)
}
}
module.exports = webpack
收集依赖
// 获取文件文件夹路径
const dirname = path.dirname(filePath)
// 存储依赖
const deps = {}
// 收集依赖
traverse(ast, {
// 内部会遍历ast 中的program.dody, 判断里面的语句类型
// 如果type:"ImportDeclaration" 就会触发当前函数
ImportDeclaration ({node}) {
// 文件相对路径 './add.js'
const relativePath = node.source.value
// 生成基于入口文件的绝对路径
const absoulatePath = path.resolve(dirname, relativePath)
console.log(node)
// 添加依赖
deps[relativePath] = absoulatePath
}
console.log(deps)
})
根据类型,获取引入文件地址:
这是打印的deps收集的依赖
编译代码
借助babel-core 中的transformFromAst
// 编译代码:将代码中浏览器不能识别的语法进行编译
const { code } = transformFromAst (ast, null, {
presets: ['@babel/preset-env']
})
console.log(code)
可以看到模块语法变成了commonJS
递归收集所有依赖
build函数是将前面的三个步骤进行了封装
const filePath = this.options.entry
// 第一次构建,得到入口文件的信息
const fileInfo = this.build(filePath)
this.modules.push(fileInfo)
// 递归收集依赖
// 遍历所有的依赖
this.modules.forEach( fileInfo => {
// deps: {
// './add': '/Users/qwr/Desktop/demo/webpack-demo/mywebpack/src/add',
// './count': '/Users/qwr/Desktop/demo/webpack-demo/mywebpack/src/count'
// }
// 取出当前文件的所有依赖
const deps = fileInfo.deps
// 遍历
for(const relativePath in deps) {
// 依赖文件的绝对路径
const absoulatePath = deps[relativePath]
// 对依赖文件进行处理
const fileInfo = this.build(absoulatePath)
// 将处理后的结果添加到modules中,后面遍历就回对fileInfo
this.modules.push(fileInfo)
}
console.log(this.modules)
})
看下结果
将依赖整理成关系图
// 整理成关系依赖图
const depsGrsph = this.modules.reduce((graph, module) => {
return {
...graph,
[module.filePath]: {
code:module.code,
deps:module.deps
}
}
}, {})
console.log(depsGrsph)
打包生成bundle.js
js 知识点: 自执行函数一定要加分号!!!!
// 生成输出资源。--js文件
generate(depsGraph) {
const bundle = `
(function (depsGraph) {
// require函数: 为了加载入口文件
function require(module) {
// 定义模块内部的require函数--引入模块内容
function localRequire (relativePath) {
// 为了找到要引入模块的绝对路径,通过require加载
return require(depsGraph[module].deps[relativePath])
};
// 定义暴露对象,(将来要暴露的内容)
var exports = {};
(function (require, exports, code){
eval(code)
})(localRequire, exports, depsGraph[module].code);
// 作为require的返回值,返回出去
// 为了后面的require函数能得到暴露的内容
return exports;
}
// 加载入口文件
require(${JSON.stringify(this.options.entry)});
})(${JSON.stringify(depsGraph)});
`
// 生成文件的绝对路径
const filePath = path.resolve(this.options.output.path, this.options.output.filename);
// 写入文件
fs.writeFileSync(filePath, bundle, 'utf-8');
}