1.基础
1. 初始化package.json
yarn init -y
安装webpack webpack-cli -D两个webpack依赖包
-D 代表开发依赖包,上线不需要
webpack 可以进行0配置
-> 打包工具 -> 输出后的结果 (js模块)
-> 打包(支持打包模块化)
手动配置webpack
-默认配置文件的名字 webpack.config.js
内置webpack-dev-server 静态服务
yarn add webpack-dev-server -D 安装
默认打包
默认执行流程
1.会先找到node_modules下的bin文件夹-> webpack.cmd 文件
2.判断有没有node.exe 有走node.exe 否则走webpack\bin\webpack.js
打包会调用webpack -> 调用webpack-cli
注意点:
webpack 如果要在命令行传参需要加多两个—
// 这样不行
npm run build --config webpack.config.my.js
// 官方说明 如需增加参数应
npm run build -- --config webpack.config.my.js
更改打包配置文件名
// package.json
"scripts": {
"build": "webpack --config webpack.config.my.js"
},
npm scripts
1. 运行多个脚本
串行&& 并行& 或||
2. 日志输出
3. 脚本传参
第一第二个元素分别是 node 路径、脚本路径, 从第三个参数开始为脚本输入的参数
process.argv.splice(2) // [ '111', '222', '333' ]
4. 环境变量 cross-env
5. 钩子 pre post
钩子执行顺序图
参考链接: https://juejin.cn/post/6917533974285778957
webpack.config.js
1.base
// webpack 是node写出来的 需要编写node的语法
// 内置node 模块
let path = require('path')
console.log (path.resolve('dist')) // E:\project\webpack\dist
module.exports = {
// 模式 默认有两种production生产 / development开发
mode : "development",
// 入口
entry : './src/index.js',
// 出口
output : {
// 打包后的文件名
filename: "bundle.[hash:8].js",
// 打包后需要放置一个位置, 路径必须是一个绝对路径
// 使用node模块path
path : path.resolve (__dirname, "dist"),
// 路径输出路径域名
publicPath: "https://www.baidu.com"
},
}
2.devServer
module.exports = {
// 静态服务
devServer: {
// 端口号
port : 8880,
// 是否需要进度条
progress : false,
// 基础路径
contentBase: './dist',
// 默认打开浏览器
open : true,
// 压缩
compress : true
},
}
3.plugins
// html插件
let HtmlWebpackPlugin = require('html-webpack-plugin')
// 处理样式抽离一个文件插件 = 1.3.0
let MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 清理dist文件夹
let { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 复制文件插件
let CopyWebpackPlugin = require('copy-webpack-plugin')
// 代码前注释插件
let { BannerPlugin } = require('webpack')
let webpac
module.exports = {
// plugins 插件 数组
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin ({
// 模板文件
template: './src/index.html',
// 文件名
filename: "index.html",
// html 压缩
minify : {
// 去掉双引号
removeAttributeQuotes: true,
// 折叠一行
collapseWhitespace : false
},
hash : true
}),
// 抽离文件插件
new MiniCssExtractPlugin({
// 抽离样式的文件名字
filename: 'main.css'
}),
// 复制插件
new CopyWebpackPlugin([
{
from: './src/doc',
to: './doc'
}
]),
new BannerPlugin('author: 杨明')
],
}
4.module loader
loader的顺序, 从右向左执行, 由下至上执行
module.exports = {
// 模块
module : {
rules: [
// js语法转换es5 babel
// 相关loader: babel-loader @babel/core 核心模块, @babel-preset-env
{
test: /\.js$/,
use : {
loader : 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
// 使用class 更高级的语法 插件
// @babel/plugin-proposal-class-properties
// @babel/plugin-proposal-decorators 装饰器
plugins: [
// '@babel/plugin-proposal-class-properties',
["@babel/plugin-proposal-decorators", {"legacy": true}],
["@babel/plugin-proposal-class-properties", {"loose": true}],
// ["@babel/plugin-transform-runtime", {
// "corejs": 2,
// }]
]
}
},
exclude: /node_modules/,
include: path.resolve(__dirname, './src')
},
{
// css-loader 处理css @import等语法
// style-loader // 插入到head标签中
// loader的特点, 单一原则
// loader的顺序, 从右向左执行, 由下至上执行
test: /\.css$/,
use : [
MiniCssExtractPlugin.loader,
'css-loader',
// autoprefixer 前缀loader
'postcss-loader'
]
},
{
// less-loader< 5.0.0
test: /\.less$/,
use : [
MiniCssExtractPlugin.loader,
'css-loader',
// 处理less 语法
'less-loader',
'postcss-loader'
]
}
]
}
}
5.optimization 优化项
// 代码压缩js
let UglifyJsPlugin = require('uglifyjs-webpack-plugin')
// 代码压缩css
let OptimizeCss = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization: {
// 压缩
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCss()
]
},
}
全局变量引入
- expose-loader 暴露到window上
- ProvidePlugin 给每个人提供一个$
- 引入,不打包 ```javascript // import $ from ‘jquery’ // expose-loader 暴露全局的loader, 内联loader // pre执行在前面的loader // normal普通的loader-内联loader // post后置执行的loader
// 在每个模块上注入 $对象 console.log ($) // console.log (window.$)
externals: { jquery: ‘$’ //引入不打包 }
<a name="o5XFQ"></a>
### 图片打包处理
```javascript
module.exports = {
module : {
rules: [
{
test: /\.html$/,
use: 'html-withimg-loader'
},
// 文件处理loader
{
test: /\.(jpg|png|gif)$/,
use : {
// url-loader好处当图片小于xxk的时候转换为base64,
// 否则正常file-loader产生真实的图片
loader: 'url-loader',
options: {
limit: 400 * 1024
}
}
},
}
}
2.配置篇
yarn init -y
yarn add webpack webpack-cli -D
配置webpack.config.js
多页应用
let path = require ('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
let { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode : 'development',
// 多入口
entry : {
home : './src/index.js',
other: './src/other.js'
},
output: {
// [name]对应的name
filename: '[name].[hash]js',
path : path.resolve (__dirname, 'dist')
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'home.html',
// 放置代码块
chunks: ['home']
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'other.html',
chunks: ['other']
})
]
}
devtool 开发调试
名称 | 特性 | 优缺点 |
---|---|---|
source-map | 映射源码,会单独创建一个map文件,出错会标识报错的行与列 | 缺点: 会创建一个大的文件, 优点: 报错信息明细精确 |
eval-source-map | 不会单独创建map文件,但会显示行与列 | 打包映射到源文件中, |
cheap-module-source-map | 创建一个map文件映射源码,但不会产生列报错 | 不便调试 |
cheap-module-eval-source-map | 不会产生文件,集成在打包后的文件中,也不会产生列 | 最优开发调试方案 |
module.exports = {
devtool: 'cheap-module-eval-source-map' // 最优方案
}
watch 实时监听打包
module.exports = {
watch: true,
watchOptions: {
// 每秒问1000次 一秒钟访问次数
poll: 1000,
// 防抖, 一直输出代码,500毫秒输出一次
aggregateTimeout: 500,
// 忽略
ignored: /node_module/
}
}
proxy 跨域代理
module.exports ={
devServer: {
open: true,
proxy: {
// 重写的方式,把请求代理到express服务器上
"/api": {
target: '192.168.16.68:7600',
pathRewrite: {
'/api': ''
}
}
}
}
}
resolve [r索] 解析
let { resolve } = require('path')
module.exports = {
// 默认路径
resolve: {
modules: [path.resolve('node_modules')],
// 入口字段 默认mian
mainFields: ['style', 'main'],
// 引入文件没有后缀名时,检测后缀 查找顺序 从左到右
extensions: ['.js', '.css', '.json', '.vue'],
alias: {
bootstrap: 'bootstrap/dist/css/bootstrap.css',
pathCss: path.resolve(__dirname, 'src/css')
}
}
}
定义环境变量
// webpack定义插件
let { DefinePlugin } = require('webpack')
module.exports = {
plugins: [
new DefinePlugin({
// 如需变为字符串JSON.stringify
DEV: JSON.stringify('production'),
FLAG: 'true',
EXP: '1+1'
})
]
}
// index.js
console.log (DEV) // production
console.log (FLAG) // true
console.log (EXP) // 2
区分不同的环境
// package.json
{
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack --config ./src/build/webpack.prod.config.js",
"build:dev": "webpack --config ./src/build/webpack.dev.config.js"
}
}
// webpack.dev.config
let { merge } = require('webpack-merge')
let base = require('./../../webpack.base')
module.exports = merge(base, {
mode: 'development',
// .....
})
//webpack.prod.config
let { merge } = require('webpack-merge')
let base = require('./../../webpack.base')
module.exports = merge(base, {
mode: 'production',
devtool: 'none',
optimization: {
minimizer: []
},
plugins: []
})
3.webpack优化
noParse-module配置
不需要解析的包, 假如我们知道包没有别的依赖,可以noParse
module.exports = {
module: {
// 不需要解析的包, 假如我们知道包没有别的依赖,可以noParse
noParse: /jquery/,
rules: [
// ...
]
}
}
IgnorePlugin-插件
// webpack内置忽略引入插件
let { IgnorePlugin } = require('webpack')
module.exports = {
plugins: [
// 忽略加载moment 下的locale所有文件
new IgnorePlugin(/\.\/locale/, /moment/)
]
}
// index.js
import moment from 'moment'
// 如果忽略后,设置不生效, 但我们可以手动引入
moment.locale('zh-cn')
import 'moment/locale/zh-cn'
let r = moment().endOf('day').fromNow()
console.log (r) // xxx小时
DllPlugin-插件-动态列js库
1.先打包固定的js库,缓存下来 DllPlugin
2.webpack.config直接DllReferencePlugin()
// path: build/webpack.config.react.js
let { resolve } = require('path')
let { DllPlugin } = require('webpack')
module.exports = {
mode: "development",
entry: {
react: [
'react',
'react-dom'
]
},
output: {
// 动态打包的名字
filename: '_dll_[name].js',
// 存放位置
path: resolve(__dirname, '../dist'),
// 导出的包名-对应关联
library: '_dll_[name]',
// 使用语法
libraryTarget: "var"
},
plugins: [
// name 需要 === library
new DllPlugin({
name: '_dll_[name]',
// manifest 用户清单
path: resolve(__dirname, '../dist', 'manifest.json')
})
]
}
// path: webpack.config.js
let { DllReferencePlugin } = require('webpack')
module.exports = {
plugins: [
// 查找依赖时先去查找manifest.json下的dll任务清单
new DllReferencePlugin({
manifest: resolve(__dirname, 'dist', 'manifest.json')
})
]
}
Happypack 多线程打包-插件
采用多线程打包
- loader使用happypack
plugins new Happypack
// webpack.config.js let Happypack = require('happypack') // yarn add happypack 安装 module.exports = { module : { rules : [ { test: /\.js$/, use : 'Happypack/loader?id=js' }, { test: /\.css$/, use : 'Happypack/loader?id=css' } ] }, plugins: [ new Happypack ({ id : 'js', use: [ { loader : 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react' ] } } ] }), new Happypack ({ id : 'css', use: ['style-loader', "css-loader"] }) ] }
webpack自带的优化
在生产模式下会自动做两个事情
tree-shaking 只有es6 import/export语法才生效
- scope hosting ```javascript // index.js
// import 语法webpack在生产环境下 会自动去掉没有引用的代码 // tree-shaking优化 只有Es6语法才会支持tree-shaking import calc from ‘./test’ // calc: { sum, minus } console.log (calc.sum (1, 2))
// scope hosting 作用域提升 let a = 1 let b = 2 let c = 3 let d = a + b + c console.log (d, ‘ ————————————-‘) const fun1 = () => { return d + a } console.log (fun1(), ‘222222222’)
// 打包后的代码 // console.log (6, “ ————————————-“); // console.log (7, “222222222”)
<a name="U4Nqo"></a>
### splitChunks 把公共代码抽离出来
1.公共抽离 common<br />2.第三方抽离 vendor
```javascript
// webpack.config.js
module.exports = {
// 优化项
optimization: {
// 分割代码块
splitChunks: {
// 缓存组
cacheGroups: {
// 公共的模块
common: {
chunks: "initial",
minSize: 1,
// 最少使用次数
minChunks: 2
},
// 第三方模块抽离
vendor: {
// 权重
priority: 1,
// 把node_modules下的依赖包抽离出来
test: /node_modules/,
chunks: "initial",
minSize: 1,
// 最少使用次数
minChunks: 2
}
}
}
}
}
懒加载 import(../xx)
依赖webpack import() 动态加载
// index.js
let btn = document.createElement ('button')
btn.innerHTML = '添加按钮'
btn.addEventListener ('click', () => {
// es6 草案中的语法, jsonp实现动态加载文件
import('./source').then (data => {
// Module {default: "杨明炜", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
console.log (data)
})
})
document.body.appendChild (btn)
热更新
HotModuleReplacementPlugin
// webpack.config.js
let webpack = require('webpack')
module.exports = {
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
4.tapable
sync 同步
SyncHook | 调用call() 同步顺序执行 |
---|---|
SyncBailHook | 调用call() 如果返回不是undefined 中止执行 |
SyncWaterfallHook | 调用call() 顺序执行,上一个结果返回给下一个 |
SyncLoopHook | 调用call() 顺序执行,任务如果不是返回undefined 继续执行当前 |
SyncHook
// 手写SyncHook方法
// 调用call() 同步顺序执行
class SyncHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tap(name, task) {
this.tasks.push (task)
}
// 队列同步执行
call(...args) {
this.tasks.forEach (task => task (...args))
}
}
let hook = new SyncHook (['name'])
// 添加任务
hook.tap ('杨明', (name) => {
console.log (name, '杨明')
})
hook.tap ('明炜', (name) => {
console.log (name, '明炜')
})
// 调用执行方法
hook.call ('你好啊')
// 你好啊 杨明
// 你好啊 明炜
SyncBailHook
// 调用call() 顺序执行如果返回undefined 才会继续往下执行
class SyncBallHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tap(name, task) {
this.tasks.push (task)
}
// 执行task 如果返回不是undefined 中止执行
call(...args) {
let ret;
let index = 0
do {
ret = this.tasks[index++] (...args)
} while (ret === undefined && index < this.tasks.length)
}
}
let hook = new SyncBallHook (['name'])
// 添加任务
hook.tap ('杨明', (name) => {
console.log (name, '杨明')
})
hook.tap ('明炜', (name) => {
console.log (name, '明炜')
return '学不动了'
})
hook.tap ('杨明炜', (name) => {
console.log (name, '杨明炜')
})
// 调用执行方法
hook.call ('你好啊')
// 你好啊 杨明
// 你好啊 明炜
SyncWaterfallHook
// 调用call() 顺序执行,上一个结果返回给下一个
class SyncWaterfallHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tap(name, task) {
this.tasks.push (task)
}
call(...args) {
let [first, ...other] = this.tasks
let result = first (...args)
other.reduce ((prev, next) => {
return next (prev, ...args)
}, result)
}
}
let hook = new SyncWaterfallHook (['name'])
// 添加任务
hook.tap ('杨明', (name, data) => {
console.log (name, '杨明', data)
return '杨明加油'
})
hook.tap ('明炜', (name, data) => {
console.log (name, '明炜', data)
return '明炜加油'
})
hook.tap ('杨明炜', (name, data) => {
console.log (name, '杨明炜', data)
return '学不动啦'
})
// 调用执行方法
hook.call ('你好啊')
// 你好啊 杨明 undefined
// 杨明加油 明炜 你好啊
// 明炜加油 杨明炜 你好啊
SyncLoopHook
// 调用call() 顺序执行,任务如果不是返回undefined 继续执行当前
class SyncLoopHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tap(name, task) {
this.tasks.push (task)
}
call(...args) {
this.tasks.forEach (task => {
let result
do {
result = task (...args)
} while (result !== undefined)
})
}
}
let hook = new SyncLoopHook (['name'])
let learnNumber = 0
// 添加任务
hook.tap ('杨明', (name) => {
console.log (name, '杨明')
return ++learnNumber === 3 ? undefined : '看不懂就多看几遍'
})
hook.tap ('明炜', (name) => {
console.log (name, '明炜')
})
hook.tap ('杨明炜', (name) => {
console.log (name, '杨明炜')
})
// 调用执行方法
hook.call ('你好啊')
// 你好啊 杨明
// 你好啊 杨明
// 你好啊 杨明
// 你好啊 明炜 undefined
// 你好啊 杨明炜 undefined
异步
AsyncParallHook | 异步 并行执行 |
---|---|
AsyncSeriesHook | 异步 串行执行 有回调 |
AsyncSeriesWaterfallHook | 异步串行下一个得到上一个返回值,返回非null, 跳出异步执行 |
AsyncParallelHook
// 异步的钩子 (串行/并行) 需要等待所有并发的异步时间执行成功后才会执行回调方法
// tapable库中有三种注册方法
// tap注册, tapAsync(cb)异步注册 tapPromise(promise)
class AsyncParallelHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tapPromise(name, task) {
this.tasks.push (task)
}
// 队列同步执行
promise(...args) {
let tasks = this.tasks.map(task => task (...args))
return Promise.all(tasks)
}
}
let hook = new AsyncParallelHook (['name'])
// 添加任务
hook.tapPromise ('node', name => {
return new Promise ((resolve, reject) => {
setTimeout (_ => {
console.log (name, 'learn node')
resolve ('node,学习完了')
}, 1000)
})
})
hook.tapPromise ('webpack', name => {
return new Promise ((resolve, reject) => {
setTimeout (_ => {
console.log (name, 'learn webpack')
resolve ('webpack,学习完了')
}, 4000)
})
})
// 调用执行方法
hook.promise('杨明').then( data => {
console.log ('end', data)
})
// 杨明 learn node
// 杨明 learn webpack
// end [ 'node,学习完了', 'webpack,学习完了' ]
AsyncSeriesHook
// 串行钩子
class AsyncSeriesHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push (task)
}
// 异步串行
callAsync(...args) {
// 获取参数的最后一个回调函数
let finalCallback = args.pop()
let index = 0
let next = _ => {
// 如果走完任务 执行回调
if (index === this.tasks.length) return finalCallback()
let task = this.tasks[index++]
task(...args, next)
}
next()
},
// 改写promise版本
promise(...args) {
let [first, ...other] = this.tasks
return other.reduce ((promise, next) => {
return promise.then (_ => next ())
}, first (...args))
}
}
let hook = new AsyncSeriesHook (['name'])
// 添加任务
hook.tapAsync ('node', (name, cb) => {
setTimeout (_ => {
console.log (name, 'learn node')
cb ('node,学习完了')
}, 3000)
})
hook.tapAsync ('webpack', (name, cb) => {
setTimeout (_ => {
console.log (name, 'learn webpack')
cb ('webpack,学习完了')
}, 1000)
})
// 调用执行方法
hook.callAsync('杨明', data => {
console.log ('end', data)
})
// 杨明 learn node
// 杨明 learn webpack
// end undefined
AsyncSeriesWaterfallHook
// 串行 瀑布钩子 把上一个的返回值传递给下一个, 最终执行回调
class AsyncSeriesWaterfallHook {
constructor() {
// 增加任务队列
this.tasks = []
}
tapAsync(name, task) {
this.tasks.push (task)
}
callAsync(...args) {
let finalCallback = args.pop ()
let index = 0
let next = (err, data) => {
// 如果是第一个把arg参数放进去, 否则给回调返回参数
if (index === this.tasks.length || (err !== null && index !== 0)) return finalCallback (data)
let task = this.tasks[index]
if (index === 0) {
task (...args, next)
} else {
task (data, next)
}
index++
}
next ()
}
}
let hook = new AsyncSeriesWaterfallHook (['name'])
// 添加任务
hook.tapAsync ('node', (name, cb) => {
setTimeout (_ => {
console.log (name, 'learn node')
cb ('error', 'node,学习完了')
}, 3000)
})
hook.tapAsync ('webpack', (name, cb) => {
setTimeout (_ => {
console.log (name, 'learn webpack')
cb (null, 'webpack,学习完了')
}, 1000)
})
// 调用执行方法
hook.callAsync ('杨明', data => {
console.log ('end', data)
})
// 杨明 learn node
// end node,学习完了
5.手写简单webpack
1.npm link
需要注意的的是, packageName 是取自包的 package.json 中 name 字段,不是文件夹名称。
使用node编写一个简易版本webpack打包
- 获取webpack.config.js 配置 config
- 编写Compiler类函数 new一个Compiler实例把config传入构造函数上缓存取来
- 调用compiler的run方法
- run方法分别执行 构建模块/发射文件两个大方法
- 构建模块
1.使用node的fs模块 读取文本内容
2.把绝对路径转换为相对路径缓存在modules以路径为key, 编译好的内容为value缓存起来
3.编译使用 babylon及traverse编译重写方法名为webpack_require
4.然后递归查找构建模块下有没有require引用
- 发射文件使用ejs模板文件生成文件,并写入到配置到output下
- loader在读取文件的时候做loader编译
项目代码: github/webpack