模块化开发 & webpack
- 1、模块化开发 & webpack
- 1.1 webpack 执行过程
- 1.1.1 初始化
- 1.1.2 编译
- 1.1.3 输出
- 2.1 webpack 基础配置
- 2.1.1 Entry
- 2.1.1.1 context
- 2.1.1.2 Entry类型
- 2.1.2 output
- 2.1.2.1 filename
- 2.1.2.2 publicPath
- 2.1.2.3 path
- 2.1.2.4 libraryTarget 和 library
- 2.1.3 module
- 2.1.3.1 loader
- 2.1.4 Resolve
- 2.1.5 总结
- 2.2 编写loader
- 2.2.1 loader基础
- 2.2.2 loader进阶
- 2.2.3 加载本地Loader
- 2.2.4 实战
- 2.3 编写plugin
- 2.3.1 Compiler 和 Compilation
- 2.3.2 事件流
为什么要将文件进行打包?
1、转换文件 兼容性
2、对众多资源处理 css font image
3、产物优化 代码压缩 代码丑化 源码安全性 减少文件体积
1、模块化开发 & webpack
问题:
1、从0-1使用 webpack 进行搭建 vue react
2、对 webpack 执行过程
3、常用的 plugins 自定义自己的插件 事件机制 tapable 发布订阅模式 事件注册 触发等
4、常用的 loaders 自定义的loader 内容 原理 babel-loader es6+es5 ast
5、优化:
-体积:采取xxx、xxx
-构建速度:采取
1.1 webpack 执行过程
1.1.1 初始化
初始化参数,后续参数合并options
1、配置项
- entry 入口 第一步从entry 开始
- module 不同文件解析内容
- loader 模块转换器 babel-loader ts-loader等
- plugin 扩展插件 构建过程 广播事件 插件监听这些事件 在特定时机做对应事情
2、实例化 compiler new Compiler(options)
-负责文件监听和启动编译 全局唯一
3、加载插件 插件的apply 对事件进行监听 compiler
1.1.2 编译
run 启动一次新的编译
compilation 模块的资源 编译生成的资源 变化的文件
1.1.3 输出
输出打包后的文件
emit
done
2.1 webpack 基础配置
2.1.1 Entry
entry是配置模块的入口,可抽象成输入,Webpack 执行构建的第一步将从入口开始搜寻及递归解析出所有入口依赖的模块。
2.1.1.1 context
Webpack 在寻找相对路径的文件时会以 context 为根目录,context 默认为执行启动 Webpack 时所在的当前工作目录。 如果想改变 context 的默认配置,则可以在配置文件里这样设置它:
module.exports = {
context: path.resolve(__dirname, 'app')
}
2.1.1.2 Entry类型
- string ‘./app/entry’ 入口模块的文件路径,可以是相对路径。
- array [‘./app/entry1’, ‘./app/entry2’] 入口模块的文件路径,可以是相对路径。
- object { a: ‘./app/entry-a’, b: [‘./app/entry-b1’, ‘./app/entry-b2’]} 配置多个入口,每个入口生成一个 Chunk
如果是 array 类型,则搭配 output.library 配置项使用时,只有数组里的最后一个入口文件的模块会被导出。
2.1.2 output
output
配置如何输出最终想要的代码。output 是一个 object,里面包含一系列配置项:
2.1.2.1 filename
配置输出文件的名称,为string 类型。如果只有一个输出文件,则可以把它写成静态不变的:
filename: 'bundle.js'
但是在有多个 Chunk 要输出时,就需要借助模版和变量了。前面说到 Webpack 会为每个 Chunk取一个名称,可以根据 Chunk 的名称来区分输出的文件名:
filename: '[name].js'
代码里的[name] 代表用内置的 name 变量去替换[name],这时你可以把它看作一个字符串模块函数, 每个要输出的 Chunk 都会通过这个函数去拼接出输出的文件名称。
内置变量除了 name 还可以包括:
- id chunk 唯一id
- name: chunk 名称
- hash Chunk 的唯一标识的 Hash 值
- chunkhash Chunk 内容的 Hash 值
输出 => filename:‘[name][hash].js’
2.1.2.2 publicPath
静态资源URL前缀 形成完整 url
filename:'[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'
2.1.2.3 path
output.path 配置输出文件存放在本地的目录,必须是 string 类型的绝对路径。
path: path.resolve(__dirname, 'dist_[hash]')
output.path 和 output.publicPath 都支持字符串模版,内置变量只有一个:hash 代表一次编译操作的 Hash 值。
2.1.2.4 libraryTarget 和 library
- libraryTarget 何种方式导出当前库
- library 导出库的名称 antd
2.1.3 module
module 配置如何处理模块。
2.1.3.1 loader
loader 何种方式处理模块
文件类型 针对不同的文件类型 如何处理
比如js用babel-loader。ts、vue、md、css、less对应使用不同loader等等,也可以使用自定义loader
示例:
module: {
rules: [
{
// 命中 JavaScript 文件
test: /\.js$/,
// 用 babel-loader 转换 JavaScript 文件
// ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
use: ['babel-loader?cacheDirectory'],
// 只命中src目录里的js文件,加快 Webpack 搜索速度
include: path.resolve(__dirname, 'src')
},
{
// 命中 SCSS 文件
test: /\.scss$/,
// 使用一组 Loader 去处理 SCSS 文件。
// 处理顺序为从后到前,即先交给 sass-loader 处理,再把结果交给 css-loader 最后再给 style-loader。
use: ['style-loader', 'css-loader', 'sass-loader'],
// 排除 node_modules 目录下的文件
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 对非文本文件采用 file-loader 加载
test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
use: ['file-loader'],
},
]
}
2.1.4 Resolve
resolve 入口模块如何找出所有依赖的模块
- alias 别名配置 :通过别名来把原导入路径映射成一个新的导入路径
// Webpack alias 配置
resolve:{
alias:{
components: './src/components/'
}
}
当你通过 import Button from 'components/button'
导入时,实际上被 alias 等价替换成了 import Button from './src/components/button'
。
以上 alias 配置的含义是把导入语句里的 components 关键字替换成 ./src/components/。
- extensions 在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。
extensions: ['.js', '.json']
当遇到 require(‘./data’) ,data省略了后缀名,先查找有没有data.js,再找data.json,都没有就会报错
2.1.5 总结
Webpack 内置了很多功能。 你不必都记住它们,只需要大概明白 Webpack 原理和核心概念去判断选项大致属于哪个大模块下,再去查详细的使用文档。
通常你可用如下经验去判断如何配置 Webpack:
- 想让源文件加入到构建流程中去被 Webpack 控制,配置 entry;
- 想自定义输出文件的位置和名称,配置 output;
- 想自定义寻找依赖模块时的策略,配置 resolve;
- 想自定义解析和转换文件的策略,配置 module,通常是配置 module.rules 里的 Loader;
- 其它的大部分需求可能要通过 Plugin 去实现,配置 plugin;
2.2 编写loader
Loader
就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。
以处理 SCSS 文件为例:
SCSS
源代码会先交给sass-loader
把SCSS
转换成CSS
;- 把
sass-loader
输出的CSS
交给css-loader
处理,找出CSS
中依赖的资源、压缩CSS
等; - 把
css-loader
输出的CSS
交给style-loader
处理,转换成通过脚本加载的JavaScript
代码;
可以看出以上的处理过程需要有顺序的链式执行,先 sass-loader
再 css-loader
再 style-loader
。 以上处理的 Webpack 相关配置如下:
module.exports = {
module: {
rules: [
{
// 增加对 SCSS 文件的支持
test: /\.scss$/,
// SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
use: [
'style-loader',
{
loader:'css-loader',
// 给 css-loader 传入配置项
options:{
minimize:true,
}
},
'sass-loader'],
},
]
},
};
一个 Loader 的职责是单一的,只需要完成一种转换。 如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。
2.2.1 loader基础
一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。
这个导出的函数的工作就是获得处理前的原内容
,对原内容执行处理后,返回处理后的内容
。
一个最简单的 Loader 的源码如下:
module.exports = function(source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
return source;
};
由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:
const sass = require('node-sass');
module.exports = function(source) {
return sass(source);
};
2.2.2 loader进阶
以上只是个最简单的 Loader,Webpack 还提供一些 API 供 Loader 调用
1、获得 Loader 的 options
在最上面处理 SCSS 文件的 Webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader。 如何在自己编写的 Loader 中获取到用户传入的 options 呢?需要这样做:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
return source;
};
2、返回其他结果
上面的 Loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西。
例如以用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码。 为了把 Source Map 也一起随着 ES5 代码返回给 Webpack,可以这样写:
module.exports = function(source) {
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};
3、同步与异步
有些场景下转换的步骤只能是异步完成
在转换步骤是异步时,你可以这样:
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};
4、处理二进制数据
module.exports = function(source) {
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;
5、缓存加速
如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};
6、其他Loader API
Webpack 官网
2.2.3 加载本地Loader
两种方式:
1、 npm link:把Loader链接到node_moduels 下
2、配置ResolveLoader // 去哪些目录下寻找 Loader,有先后顺序之分
module.exports = {
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}
2.2.4 实战
创建名为comment-require-loader
自定义Loader作用是把 @require '../style/index.css'
转为require('../style/index.css');
该 Loader 的使用方法如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['comment-require-loader'],
// 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换
include: [path.resolve(__dirname, 'node_modules/imui')]
}
]
}
};
具体实现时:
function replace(source) {
// 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');
return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}
module.exports = function (content) {
return replace(content);
};
2.3 编写plugin
一个最基础的 Plugin 的代码是这样的:
class BasicPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}
// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
compiler.plugin('compilation',function(compilation) {
})
}
}
// 导出 Plugin
module.exports = BasicPlugin;
在使用这个 Plugin 时,相关配置代码如下:
const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins:[
new BasicPlugin(options),
]
}
2.3.1 Compiler 和 Compilation
在开发 Plugin 时最常用的两个对象就是 Compiler
和 Compilation
,它们是 Plugin
和 Webpack
之间的桥梁。 Compiler
和 Compilation
的含义如下:
Compiler
对象包含了 Webpack 环境所有的的配置信息,包含options
,loaders
,plugins
这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象;
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
2.3.2 事件流
Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable
来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
在开发插件时,还需要注意以下几点:
- 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
- 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
- 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
compiler.plugin('emit',function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});