关于webpack
webpack 其实就是一个打包工具, 他可以把css, js, 图片等等的东西都打包成一个bundle,从entry开始递归分析他的依赖图,把应用到的每一个模块打包成一个或多个bundle
webpakc 主要依赖下面几个配置
entry
: 主入口文件output
: 输入文件的位置modules
: 里面配置的是loader, 我们可以想象loadder 为一名翻译官,把各种类型文件都翻译成浏览器可以识别的东西plugins
: 插件,我觉得webpack 的强大之处在于他的插件,plugin 可以针对在webpack不同的时期做不同的工作,比如CleanWebpackPlugin
可以在打包之前删除清理指定目录
webpack 基础配置
const path = require('path');
export default {
entry: './src/index.js', // 入口文件
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
modules: ['node_modules'], // 告诉 webpack 解析模块时应该搜索的目录。
// 配置别名
alias: {
'@': path.resolve(__dirname, 'src'), // 指定src的别名为 ‘@’
},
ententions: ['.js', '.css'], // 添加文件猴嘴
},
// 定义开发环境下的webpack-dev-server 其实就是动态更新
// 此时没有加载 HotModuleReplacementPlugin 的时候是通过loaction.reload()重新加载网页的,但有个缺点就是不能记录状态
devServer:{
contentBase: path.resolve(__dirname, 'dist'),
open: true,
port: 8000,
hot: true
},
treeShaking: true, // 这里表示将没用过的代码自动删除掉
optimization:{
splitChunks: {
cacheGroups: {
vendor: {
test: /node_modules/,
priority: 1, // 数字越大,优先级越高
minChunks: 2, // 表示至少有两个js同事引用的时候,就会打包成vendor。js
minSizes: 0, // 表示最小的大小
}
}
},
},
modules: {
noParse: /jquery/, // webpack 优化, 不去递归jquery的依赖库
rules: [
{
test: /\.css$/,
use: [
// 请记住loader 的运行顺序是从下到上,从右到左,
// 另一种模式是内敛模式, import Styles from 'style-loader!css-loader?module!./styles.css', 忠中模式通过 ! 分割loadder,
// 'style-loader!css-loader?module!./styles.css'.split("!") => ["style-loader", "css-loader"]
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
},
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
// 如果使用happypack的话,多线程打包,此时下面就要修改该成
// use: 'Happypack/loader?id=js'
use: {
loader: 'babel-loader',
option: {
cacheDirectory: true, // 开启js 打包优化
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [require('@babel/plugin-transform-object-rest-spread')]
},
}
}
]
},
mode: 'development', // 指定环境,
plugins: [
// 编译的时候指定全局变量,我们可以根据这个去定义当前环境是开发环境还是线上环境,定义不通的行为,比如url
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true), // 此时传过去的 PRODUCTION 是 字符串 “true”
VERSION: JSON.stringify("5fa3b9"), //
}),
// 多线程打包, 要是对css也启动多线程的话,再创建一个happypack, id为css
new Happypack({
id: 'js',
use: [{
loader: 'babel-loader',
option: {
cacheDirectory: true, // 开启js 打包优化
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [require('@babel/plugin-transform-object-rest-spread')]
},
}]
}),
// ignorePlugin, 针对某个包的依赖不进行打包,比如moment, locale 是moment的语言包,要是我们只使用zh-cn 那么我们可以忽略掉其他,所以此时忽略掉locale
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// 下面是热更新
new webpack.NamedModulesPlugin(), // 告诉webpack 哪个模块更新了
new webpack.HotModuleReplacementPlugin(); // 进行热更新
]
}
热更新
import test from './test';
if(module.hot) {
module.hot.accept('./test', () => {
console.log('文件已更新');
require('./test');
})
}
我们总结一下上面关于打包优化的几种方式吧
- 配置resolve 减少目录的搜索路径
- 同样的在loader 中设置 include 和
exclude
指定loader 编译的目录 - 设置modules 下的
noParse
属性, 这个可以在打包的时候不检查某js
的依赖,这样就可以减少打包时间了 - 配置DllPlugin 用动态链接库的形式打包,这样的话会根据目录下的manifest.json 进行二次打包, 二次打包不会再对已生成的动态链接库进行打包
- 使用happyPack 用多线程打包
tapable
webpack 本质上是一种事件流机制,它的工作流程就是把各个插件串联起来, 他的核心就是tapable, tapable 有点像nodejs 的event库, 就是观察者模式
先来看看一个简单的events 库
class EventBus {
constructor() {
this.maps = {}
}
on(name, fn) {
this.maps[name] = fn;
}
fire(name, data) {
this.maps[name] && this.maps[name](data);
}
}
// 测试
const eventBus = new EventBus();
eventBus.on("click", (data) => {
console.log("click", data)
})
eventBus.fire("click", {a: 1, b: 2})
简单的观察者模式
发布订阅其实很简单, 我可以想象成天文台, 当温度改变时, 天文台的数据改变(changes)的时候,我们用户需要做什么,他下雨了,我们需要收衣服,
- 被观察者是 天文台,
- 观察者 是我们用户, 具体做法是我们要收衣服, 就是对应下面的update, 简单说就是具体的做法就是观察者了
class Subject {
constructor() {
this.watchers = []
}
addWatch(watcher) {
console.log(this.watcher)
this.watchers.push(watcher)
}
removeWatcher(watcher) {
let index = this.watchers.indexOf(watcher);
if(index > -1) {
this.watchers.splice(index, 1)
}
}
notify() {
this.watchers.forEach((watcher) => watcher.update())
}
}
class Watcher {
subscribeTo(subject) {
subject.addWatch(this);
}
update() {}
}
let subject = new Subject()
let watcher = new Watcher()
watcher.update = function() {
console.log('observer update')
}
watcher.subscribeTo(subject) //观察者订阅主题
let watcher2 = new Watcher()
watcher2.update = function() {
console.log('我是另一个观察者,我要做其他事情')
}
watcher2.subscribeTo(subject)
subject.notify()
实现 SyncHook
我们以上面的例子,实现一个SyncHook
class SyncHook {
constructor(args) {
this.tasks = []
}
// 绑定时间
tap(name, fn) {
this.tasks.push(fn)
}
// 运行函数, 在tapabel
call(...args) {
this.tasks.forEach((task) => task(...args));
}
}
let hook = new SyncHook(['name']) // ['name'] 指的是我在创建hook的时候, 我tap需要传递的参数
hook.tap("test", (name) => {
console.log('test', name)
})
hook.tap("test2", (name) => {
console.log("test2", name)
})
hook.call("hello"); // 这里的hello 对应的是上面的name
webpack 原理
webapck其实就是自己实现了一个require方法,这里需要对AST
进行一部分的了解, AST就是抽象语法树, 简单说就是将 js 转换成 语法树,转换成 方法, 变量等等的属性
我们看看AST 的步骤
AST
将js
转换成 语法树- 修改语法树的值
- 将
AST
转换成浏览器可以识别的 语法
AST
依赖包
- babylon 将 源码 解析成
AST(抽象语法树)
- @babel/traverse 遍历 AST 中的节点
- @babel/types 替换 AST 节点
- @babel/generator 将替换的结果生成成js
我们想想webpack 的运行过程, 我们首先配置 webpack.config.js
, 然后运行的是 webpack --config webpack.config.js
然后webpack 会根据 entry
入口文件
进行分析,对它进行AST
解析, 如果entry入口文件
还有require
, 那么继续进行依赖遍历。
loader
其实loader 就是一个方法,我们看两个例子, 一个是less-loader, 另一个是style-loader
其中 loader-utils
获取loader 的参数就是 loader 的 options
less-loader
// less-loader
/**
* 我们less-loader 当然要转换成css,那么我们使用的是less.render
* 下面使用less那么肯定需要 npm install less -=save-dev
* @param {string} source 这里的source就是指 less源码
*/
const loaderUtils = require('loader-utils');
function loader(source) {
let css = "";
// loaderUtils.getOption(this) 可以拿到他的参数
less.render(source, (err, lessSource) => {
css = lessSource.css
})
return css;
}
style-loader
/**
* 我们style-loader 的作用是将css 写在html 的head 下面的style标签下
*/
function loader(source) {
let styles = `
const el = document.createElement("style");
const css = ${source.replace(/\s*/g, "")}
el.innerHTML = css;
document.head.appendChild(css);
`
return styles
}
plugin
webpack 是基于tapable事件流, 你把 plugin 想象成在webapck 中不同的生命周期做不同的事情,我们看看webpack 的hooks吧
- entryOption 入口hooks
- compile 编译时期
- afterCompile 完成编译后
- afterPlugins 插件完成编译后
- run 运行
- emit 生成编译文件时
- done 执行完成
自定义plugin
class Plugin() {
apply(compiler) { // 此时的compiler 是webpack实例
compiler.hooks.done.tap("run", () => {
console.log("此时是webpack 运行时运行的时间")
})
compiler.hooks.done.tap("name", () => {
console.log("此时是注册事件,指的是在整个wepack执行完成之后的回调函数")
})
}
}