1.基础
1. 初始化package.jsonyarn 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-env5. 钩子 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\distmodule.exports = {// 模式 默认有两种production生产 / development开发mode : "development",// 入口entry : './src/index.js',// 出口output : {// 打包后的文件名filename: "bundle.[hash:8].js",// 打包后需要放置一个位置, 路径必须是一个绝对路径// 使用node模块pathpath : 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.0let MiniCssExtractPlugin = require('mini-css-extract-plugin')// 清理dist文件夹let { CleanWebpackPlugin } = require('clean-webpack-plugin')// 复制文件插件let CopyWebpackPlugin = require('copy-webpack-plugin')// 代码前注释插件let { BannerPlugin } = require('webpack')let webpacmodule.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.0test: /\.less$/,use : [MiniCssExtractPlugin.loader,'css-loader',// 处理less 语法'less-loader','postcss-loader']}]}}
5.optimization 优化项
// 代码压缩jslet UglifyJsPlugin = require('uglifyjs-webpack-plugin')// 代码压缩csslet 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>### 图片打包处理```javascriptmodule.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 -yyarn 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]对应的namefilename: '[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

