模块化工具的由来
- ES Modules存在环境兼容问题
- 模块文件过多,网络请求频繁
- 所有的前端资源都需要模块化
yarn init -y
yarn add webpack-cli --dev
yarn webpack // 在4.0以后支持0配置打包;`scr/index.js`->`dist/main.js`
配置文件
新建webpack.cinfig.js的配置文件
const path = require('path')
module.exprots = {
entry:'./src/main.js', // 入口文件
output:{
filename: 'bundle.js', // 生成文件名
path:path.join(__dirname,'output') // 生成文件路径(必须是绝对路径)
}
}
工作模式
可以理解为针对不同的环境的几组预设的配置,简化了配置模式的复杂程度
// mode 在webpack5中不用写
yarn webpack --mode production // 默认,优化打包结果(压缩)
yarn webpack --mode development // 开发模式,优化打包速度(添加调试时的辅助)
yarn webpack --mode none // 原始打包,不做任何额外处理
在配置文件中添加mode:'development'
属性就会根据配置当中的模式去工作了
原理
依赖关系树(递归)
生成代码是一个立即执行函数,这个函数是webpack的工作入口,他接收一个modules的参数,调用时传入一个数组(该数组中的每个元素都是一个参数列表相同的函数,这里的函数就对应着我们源代码中的模块
,我们的每一个模块都会被包裹到这样的一个函数当中,从而实现模块的私有作用域)
模块id就是模块数组中的元素下标(这里才开始加载我们在源代码中的入口模块)
资源模块加载(loader)
webpack在所有模块打包之前,将模块根据配置交给不同的loader去处理,最后将处理的结果打包到一起
相当于工厂里的生产车间,处理和加工打包中的资源文件
- 编译转换类
- 文件操作类
- 代码检查类
通过不同的Loader就可以加载任何类型的资源
yarn add css-loader style-loader --dev
作用就是将css文件转换为一个js模块;创建style标签追加到页面上
module:{
rules:[
{
test:/.css$/, // 正则匹配
use: [ // 执行顺序是从后往前
'style-loader'
'css-loader'
]
}
]
}
导入资源模块
根据代码的需要动态导入资源;需要资源的不是应用,而是代码
js驱动整个前端应用
逻辑合理,js确实需要这些资源文件
确保上线资源不缺失,都是必要的
import './heading.css'
export default () => {
const element = document.createElement('h2')
element.textContent = 'Hello world'
element.classList.add('heading')
element.addEventListener('click',()=>{
alert('Hello world')
})
return element
}
文件资源加载器
大多数的加载器都是类似于css-loader,将css模板转换为js代码的方式去工作。还有些文件,例如图片和字体,这些文件是没有办法去用js文件的方式来表示的,这类文件需要用到file-loader
yarn add file-loader --dev
{ // webpack-dev-server使用的是内存中的打包文件,并不是webpack命令打包后的路径;
// webpack-dev-server在不设置publicPath的情况下,将默认输出bundle.js到根目录
publicPath:'dist/' // 该配置能帮助你为项目中的所有资源指定一个基础路径(虚拟打包路径,就是说文件夹不会真正生产,而且在8080端口虚拟生成)
},
{
test:'/.png$/',
use:'file-loader'
}
处理ES6
因为模块打包需要,所以处理import和export
- webpack只是打包工具
- 加载器可以用来编译转换代码
yarn add babel-loader @babel/core @babel/preset-env --dev
{
test:/.js$/,
use:{
loader:'babel-loader', // 是一个平台,需要通过不同的插件来转换
options:{
presets:['@babel/preset-env'] // 插件的集合
}
}
}
模块加载方式
- EsModules
- CommonJs
require函数载入EsModules,对于默认导出。引入时需要用到default
const createHeading = require('./heading.js').default
Loader加载的非javaScript也会触发资源加载,例如css-lodaer加载的css文件样式代码中的@import指令和url函数,html代码中图片标签的src属性
// css文件中
@import url(reset.css);
{background-image: url(background.png);}
// js文件中
import footerHtml from './footer.html'
document.write(footerHtml )
{
test:/.png$/,
use:{
loader: 'url-loader',
options:{
limit: 10*1024 // 10kb
}
}
},
{
test:/.html$/,
use:{
loader: 'html-loader',
options:{
attrs: ['img:src','a:href']
}
}
}
开发一个自己的loader
// markdown-loader.js
const marked = require('marked')
const html = marked(source)
// return `module.exports = ${JOSN.stringfiy(html)}` // return的必须是js代码
return html
module:{
rules:[
{
test:/.md$/,
use:[
'html-loader',
'./markdown-loader'
]
}
]
}
插件机制
plugin解决其他自动化工作
- 清除dist目录
yarn add clean-webpack-plugin --dev
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
new CleanWebpackPlugin()
]
- 自动生成使用bundle.js的HTML
yarn add html-webpack-plugin --dev
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins:[
new HtmlWebpackPlugin({
title:'Webpack Plugin Sample',
meta:{
viewport: 'width=device-width'
}, // 原数据标签
template:'./src/index.html' // 使用模板
}), // 自定义配置选项
// 同时输出多个页面文件
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
// 在src下新建index.html模板页面
<h1><%= htmlWebpackPlugin.options.title %></h1>
- 复制静态文件至输出目录
yarn add copy-webpack-plugin --dev
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 开发阶段最好不要使用这个插件
plugins:[
new CopyWebpackPlugin([
// 'public/**'
'public'
])
]
开发一个自己的插件
plugin通过钩子机制实现
要求是一个函数或者是一个包含apply方法的对象
// 清除.js注释
class MyPlugin {
apply(compiler){ // 核心对象,compiler包含所有的构建配置信息
console.log('MyPlugin启动时自动调用')
compiler.hooks.emit.tap('MyPlugin', compilation => { // 插件的名称,挂载的函数
// compilation => 可以理解为此次打包的上下文
for (const name in compiler.assets){ // compiler.assets写入资源信息
// conole.log(name) // 文件名
console.log(compilation.assets[name].source()) // 拿到内容
if(name.endsWith('.js')){
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g,'')
compilation.assets[name] = {
source:() => withoutComments, // 返回新的内容
size:() => withoutComments.length // webpack内部要求的方法,大小
}
}
}
}) // .tap方法去注册一个函数
})
}
}
plugins:[
new MyPlugin()
]
webpack增强开发体验
自动编译
yarn webpack --watch
serve dist
自动刷新
browser-sync dist --files "**/*"
Dev Server
他提供了一个http Server, 集成了 [自动编译] 和 [自动刷新] 等功能
yarn add webpack-dev-server --dev
yarn webpack-dev-server --open
默认只会serve打包输出文件,只要是webpack输出的文件都可以直接被访问到,其他静态资源文件也需要serve
devServer: {
contentBase: './public' // 额外为开发服务器指定查找资源目录
}
代理api
devServer: {
poxy:{
'/api':{
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite:{ // 代理路径规则的重写
'^/api':''
},
// 不能使用localhost:8080 (浏览器请求)作为请求github的主机名,以代理主机名去请求
changeOrigin: true
}
}
}
resolve(解析)
resolve 用于设置模块如何解析,常用配置如下:
alias:配置别名,简化模块引入;
extensions:在引入模块时可不带后缀;
symlinks:用于配置 npm link是否生效,禁用可提升编译速度。
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.d.ts'],
alias: {
'@': paths.appSrc,
},
symlinks: false,
}
}
Source Map
源代码地图
// 因开发环境与生成环境的代码不一致,需要调试代码
// # SourceMappingURL = jquery-3.4.1.min.map
加上这行注释,请求Source Map文件根据Source Map文件映射的关系,逆向解析出源代码找到问题所在
devtool: 'none' // eval-cheap-module-source-map
首次打包速度慢无所谓,因为有dev server重写打包速度快
HMR
模块热替换(热拔插)
热替换只将修改的模块实时替换至应用中,不必完全刷新应用
我们需要手动处理js模块更新后的热替换
const webpack = require('webpack')
devServer: {
hot: true // hotOnly 如果出现问题都不会自动刷新
}
plugins:[
new webpack.HotModuleReplacementPlugin()
]
在打包的入口文件中
module.hot.accept('./editor',() => { // 注册
console.log('editor 模块更新了')
})
处理图片模块热替换
module.hot.accept('./better.png',() => {
img.src = background
console.log(background)
})
不同的环境创建不同的配置
- 配置文件根据环境不同导出不同配置(中小项目)
module.exports = (env, argv) => { // 环境名,运行cli的所有参数
const config = {}, // 公用配置
if(env === 'production'){
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
yarn webpack --env production
- 一个环境对应一个配置文件(大型项目)
创建 webpack.common.js
webpack.dev.js
webpack.prod.js
yarn add webpack-merge --dev
const common = require('./webpack.common')
const merge = require('webpack-merge') // 合并配置
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode:'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
DefinePlugin
在webpack4下的production模式中,自动开启了一些很通用的优化功能, 开箱即用,但是忽略了需要了解的东西
为代码注入全局成员
process.env.NODE_ENV // production模式中默认注入
const webpack = require('webpack')
plugins:[
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
optimization(优化)
optimization 用于自定义 webpack 的内置优化配置,一般用于生产模式提升性能,常用配置项如下:
minimize:是否需要压缩 bundle;
minimizer:配置压缩工具,如
TerserPlugin、OptimizeCSSAssetsPlugin; splitChunks:拆分 bundle;
runtimeChunk:是否需要将所有生成 chunk 之间共享的运行时文件拆分出来。
module.exports = {
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin(),
],
splitChunks: {
// include all types of chunks
chunks: 'all',
// 重复打包问题
cacheGroups:{
vendors:{ //node_modules里的代码
test: /[\\/]node_modules[\\/]/,
chunks: "all",
name: 'vendors', //chunks name
priority: 10, //优先级
enforce: true
}
}
},
},
}
Tree-shaking![在这里插入图片描述](https://img-blog.csdnimg.cn/da094d1a21fb45f3bc7cc8ca2ef29f91.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5aSP5pqW5Yas5YeJ,size_20,color_FFFFFF,t_70,g_se,x_16)
未引用代码 (dead-code) 移除掉 ,production模式自动开启
optimization: {
usedExports: true, // (标记树叶)只导出外部使用了的成员
minimize: true // (摇掉未引用的代码) 压缩
}
Tree-shaking前提是ES Modules,就是说交给webpack打包的代码必须使用ESM;但是在babel转换中可能将es转com(但是在最新的babel-loader中,自动关闭了es转换com的插件)
options:{ presets: [ [ '@babel/preset-env', { modules: 'commonjs' } ] ] } // 强制开启es转换com的插件,可设置为false
Scope Hoisting
尽可能的将所有模块合并输出到一个函数中(在这之前是一个模块对应一个函数)
即提升了运行效率,又减少了代码的体积
optimization:{
concatenateModules: true
}
sideEffects
在webpack4中新增的新特性,通过配置的方式去
标识代码是否有副作用
,为Tree-shaking提供更大的压缩空间
副作用:模块执行时除了导出成员之外所作的事情
打包后,所有组件模块都被打包(这里就需要用到sideEffects)
optimization:{
sideEffects: true, // production模式自动开启
}
在package.json中
"sideEffects: false" // 标识package.json影响的项目当中的所有代码都没有没有副作用,一旦没有副作用就会被移除掉
注意:确保你的代码真的没有副作用
"sideEffects" : [
"./src/extend.js",
"*.css"
]
Code Splitting
代码分包/代码分割
因为所有的代码最终都被打包到一起,体积会特别大 并不是每个模块在启动时都是必要的。分包,按需加载
多入口打包
使用于多页面应用
一个页面对应一个打包入口,公共部分单独提取
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'none',
entry:{ // 添加2个不同的打包入口
index: './src/index.js',
album: './src/album.js'
}
output:{ // 输出2个不同的js文件
filename: '[name].bundle.js' // 入口文件名字
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
template: '['index']',
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
template: '['album']',
})
]
}
提取公共模块
optimization: {
splitChunks: {
chunks: 'all' // 所有的公共模板提取
}
}
动态导入
魔法注释
动态导入名称是随机生成的序号,我们想让每个模块的名字对应到文件名上就需要用魔法注释
import(/*webpackChunkName: 'posts' */'./posts/posts').then() // 如果2个模板名称一致,则打包到一起
MinCssExtractPlug
提取css到单个文件(css的按需加载,文件小于150kb没必要这样,因为会多一次请求)
yarn add mini-css-extract-plugin --dev
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module:{
rules:[
{
use:[
MiniCssExtractPlugin.loader, // link标签的形式注入
'css-loader'
]
}
]
}
plugins:[
new MiniCssExtractPlugin({
filename: '[name].bundle.css'
})
]
OptimizeCssAssetsWebpackPlugin
压缩输出css文件
yarn add optimize-css-assets-webpack-plugin --dev
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
plugins:[
new OptimizeCssAssetsWebpackPlugin() // 任何环境下都会工作
]
// 应该在生产环境下配置,但是有个问题他会覆盖掉js压缩器,
yarn webpack --mode production
optimization:{
minimizer:[
new OptimizeCssAssetsWebpackPlugin()
new TerserWebpackPlugin()
]
}
yarn add terset-webpack-plugin --dev // 下载js压缩插件
const TerserWebpackPlugin = require('terset-webpack-plugin')
Hash
生产模式下,文件名使用Hash
filename: '[name]-[hash].bundle.css' // 当文件修改时,打包hash会发生变化
filename: '[name]-[chunkhash].bundle.css' // 同步打包,有效的解决了文件缓存的文件,精确的找到了文件位置
filename: '[name]-[contenthash].bundle.css' // 不同文件有不同文件的hash名