webpack4
webpack4实践内容,每个包添加对应版本,避免打包时出现版本不兼容问题。
esm考虑兼容性polyfill
browser-es-module-loader 进行解析 babel-browser-build 进行转换兼容处理 promise-polyfill(不支持promise)
// nomodule 可以避免执行两次 一次是转换后的执行
<script nomodule src="xxxcdn"></script>
Node 环境使用esm
老的node版本需要进行实验特性运行 ,node12+ 版本可以支持
node --experimental-modules .\index.mjs
模块相互
ESM中可以导入commonjs的导出 反之不行,commonjs始终导出一个默认成员
mjs和cjs差别
// .mjs
import { fileURLToPath } from 'url'
import { dirname, basename, extname } from 'path'
// file:///C:/Users/admin/Desktop/Git-repositories/npm/src/use.mjs
console.log(import.meta.url)
const __filename = fileURLToPath(import.meta.url)
//转换成C:\Users\admin\Desktop\Git-repositories\npm\src\use.mjs
console.log('__filename', __filename)
//C:\Users\admin\Desktop\Git-repositories\npm\src
const __dirname = dirname(__filename)
console.log('__dirname', __dirname)
//获取扩展名 和全名
console.log(basename(__filename))
console.log(extname(__filename))
// .cjs
const path = require('path')
//获取扩展名 和全名
console.log(path.basename(__filename))
console.log(path.extname(__filename))
console.log('require', require)
console.log('module', module)
console.log('exports', exports)
// 文件的绝对路径
console.log('__filename', __filename)
// 文件所在文件目录
console.log('__dirname', __dirname)
Babel 低版本兼容
安装依赖 core是核心库 preset-env是最新特性插件的集合 具体转换使用的是插件
yarn add @babel/node @babel/core @babel/preset-env --save-dev
部分安装的插件
创建文件使用 yarn babel-node index.js --presets=@babel/preset-env 进行编译执行 没有预设执行会失败
// index.js
import name from './module.js'
console.log('打印***name', name)
// module.js
const name = 'module'
export default name
使用.babelrc 配置json文件进行设置 yarn babel-node index.js
// 使用预设
{
"presets": [
"@babel/preset-env"
]
}
// 使用插件
{
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
快速上手webpack4
- 安装 yarn add webpack@4.40.2 webpack-cli@3.3.9 --dev
- 创建文件。开启服务 显示HELLO 如果不添加type就会报错,使用webpack进行打包,会进行兼容type则不需要
//src/index.js
import createElement from './module.js'
createElement(document.body)
// src/module.js
const createElement = parentNode => {
const node = document.createElement('div')
node.innerText = 'HELLO'
parentNode.appendChild(node)
}
export default createElement
// /index.html
<script type="module" src="./src/index.js"></script>
// 打包后文件
<script src="./dist/index.js"></script>
运行node src/index. js报错 两种解决方式 这就是package中为什么配置type为module的原因
-
运行命令 yarn webpack
- 出口文件自动生成dist/main.js
- 默认入口文件是src/index.js
- 会报warning 未指定打包环境 可通过 yarn webpack --mode(可选=)development/production/none
-
webpack.config.js 配置文件 因为是在node环境下运行,使用module.exports
- 出口入口配置环境配置
-
const path = require('path') module.exports = { // 打包环境配置 yarn webpakc时对应的环境 mode:'production', // 使用相对路径 entry: './src/module.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output') } }
- 资源模块加载 css css-loader@3.2.0打包解析 入口改成css的
- style-loader@1.0.0 生成style标签插入 主入口改成js,使用import导入css文件
- 通过loader可以加载任何资源文件
const path = require('path')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
// 从右到左执行
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
- 图片资源加载 file-loader@4.2.0
图片不显示,资源根目录不正确,配置publicpath
const path = require('path')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/' // /不能省略
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: 'file-loader'
}
]
}
}
- Data URLS指url包含数据 图片进行base64编码 url-loader@2.2.0
根据dataURL直接进行解析 例如
data:text/html;charset=UTF-8,<h1>html content</h1>
data:image/png;base64,xxxx//需要url-loader实现
- 体积小进行转换成data url 减少请求次数
- 大文件单独提取存放 提高加载速度
const path = require('path')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/' // /不能省略
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: 'url-loader'
}
]
}
}
如果限制了大小,超出的还是会使用file-loader去处理,所以不能删除
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: {
loader: 'url-loader',
options: {
limit: 40 * 1024 //10KB
}
}
}
-
常用加载器分类
- 编辑转换器 css-loader 生成js代码
- 文件操作类 file-loader 拷贝到输出的目录
- 代码检查类 eslint-loader
-
webpack和ES5 babel-loader@8.2.4 @babel/core @babel/preset-env
module: {
rules: [
{
test: /.js$/,
// use: 'babel-loader'
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
}
打包需要,webpack默认处理了import和export,不会处理转换js的新特性
转换后
-
webpack加载资源的方式
- ES modules标准的import声明
- 遵循commonjs标准的require函数( esm使用默认导出,使用require导入 必须使用require().default进行导入)
- 遵循AMD标准的define函数和require函数
- *样式代码中的@import指令和url函数
- 执行过程是先使用css-loader进行加载,遇到url使用url-loader进行加载
-
//index.js import './index.css' // index.css body { min-height: 100vh; background-image: url(sea.jpg); background-size: cover; } // webpack.config.js const path = require('path') module.exports = { mode: 'none', // 使用相对路径 entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' // /不能省略 }, module: { rules: [ { test: /.js$/, // use: 'babel-loader' use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, { test: /.css$/, use: ['style-loader', 'css-loader']// 1. 先解析 }, { test: /.(jpeg|png|gif|webp|jpg)$/, use: { loader: 'url-loader', // 2.遇到css中url进行解析 options: { limit: 40 * 1024 //10KB } } } ] } } //--- 第二种css中导入css文件 //reset.css * { margin: 0; padding: 0; } // index.css @import url(reset.css)
- e. *HTML代码中图片标签的src属性(默认)a的href属性需要进行配置
- 解析html使用html-loader@0.5.5
-
// footer.html <footer> <img src="./sea.jpg" alt="sea" width="250" /> </footer> //index.js import footer from './footer.html' document.write(footer) //webpack.config.js const path = require('path') module.exports = { mode: 'none', // 使用相对路径 entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' // /不能省略 }, module: { rules: [ { test: /.js$/, // use: 'babel-loader' use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, { test: /.css$/, use: ['style-loader', 'css-loader'] }, { test: /.(jpeg|png|gif|webp|jpg)$/, use: { loader: 'url-loader', options: { limit: 40 * 1024 //10KB } } }, { test: /.html$/, use: 'html-loader' } ] } } // 默认支持img:src 属性 { test: /.html$/, use: { loader: 'html-loader', options: { attrs: ['img:src', 'a:href'] // 必须全部配置 且图片必须是file loader处理不能是data-url } } }
开发一个loader
loader是资源文件的输入到输出的转换 ,对同一资源依次使用多个loader处理。工作原理:
source-> markdown-loader ->xxx.loader ->结果 返回一个js代码
// about.md
# markdown
这是一个 markdown 文本
// index.js
import about from './about.md'
// 转换成html文件
console.log('打印***about', about)
// / markdown-loader.js
// 导出函数 输入资源文件的内容
module.exports = source => {
console.log('打印***source', source)
return 'markdown-loader 转换结果' // 打包正常运行出错
return `console.log('markdown-loader 转换结果') `// 正常
}
// webpack
const path = require('path')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/' // /不能省略
},
module: {
rules: [
{
test: /.md$/,
use: './markdown-loader'
},
]
}
}
正常返回js代码
两种处理方式,1.直接返回导出结果 安装一个marked@0.7.0 解析
//markdown-loader.js
// 导出函数 输入资源文件的内容
const marked = require('marked')
module.exports = source => {
console.log('打印***source', source)
const html = marked(source)
return `module.exports = ${JSON.stringify(html)}`
// return `export default ${JSON.stringify(html)}`
}
2.返回一个html 交给html-loader@0.5.5再去处理
//markdown-loader.js
// 导出函数 输入资源文件的内容
const marked = require('marked')
module.exports = source => {
console.log('打印***source', source)
const html = marked(source)
return html
}
// webpack
{
test: /.md$/,
use: ['html-loader', './markdown-loader']
}
插件机制
解决加载资源外其他自动化工作,如清除dist 拷贝资源文件 压缩代码
- 自动清除原打包目录 clean-webpack-plugin@3.0.0
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/'
},
plugins: [new CleanWebpackPlugin()]
}
- html-webpack-plugin@4 自动生成使用bundle.js的html
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
publicPath: 'output/' // /不能省略
},
plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()]
}
进行打包后生成的html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="output/bundle.js"></script></body>
</html>
启动服务请求路径和之前的publicPath问题
去除publicPath 正常
进行配置
new HtmlWebpackPlugin({
title: 'pms',// 名称
filename: 'pms.html',// 文件名
meta: {
viewport: 'device-width'
}
})
如果html配置过多,使用模板进行配置,新建src/index.html,去除html-loader的配置
模板语法没有被正常解析, 因为配置文件中用到了html-loader, 是的模板index.html中的配置被当做字符串处理. 而使用html-loader多用来处理 component页面. , 如果有页面同时需要html-loader和html-webpack-plugin处理不可以
// 模板的html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 模板语法 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>
// webpack
new HtmlWebpackPlugin({
title: 'pms444', // 名称
template: 'src/index.html'
})
输出多个页面文件
plugins: [
new CleanWebpackPlugin(),
// index.html
new HtmlWebpackPlugin({
title: 'pms444', // 名称
template: 'src/index.html'
}),
// about.html 默认生成
new HtmlWebpackPlugin({
filename: 'about.html'
})
]
- copy-webpack-plugin@5.0.4 复制文件或目录到打包后的文件
new CopyWebpackPlugin(['public']) // 文件拷贝到输出目录
开发一个plugin
钩子机制,必须是一个函数或包含apply方法的对象,通过在生命周期的钩子中挂载函数实现扩展
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 去除注释
class MyPlugin {
apply(compiler) {
console.log('MyPlugin 启动')
// 在emit钩子时 注册插件执行
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation 理解为打包的上下文 是一个对象
// name 每个文件的名称 ,compilation.assets[name].source()内容
for (const name in compilation.assets) {
// console.log('打印***name', name)
// console.log('打印***内容', compilation.assets[name].source())
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(//**+*//g, '')
// source和size是必须
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
// publicPath: 'output/' // /不能省略
},
module: {
rules: [
{
test: /.js$/,
// use: 'babel-loader'
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 //10KB
}
}
}
// {
// test: /.html$/,
// use: {
// loader: 'html-loader',
// options: {
// attrs: ['img:src', 'a:href']
// }
// }
// }
]
},
plugins: [
new CleanWebpackPlugin(),
// index.html
new HtmlWebpackPlugin({
title: 'pms444', // 名称
template: 'public/index.html'
}),
// about.html 默认生成
new HtmlWebpackPlugin({
filename: 'about.html'
}),
new CopyWebpackPlugin(['public']), // 文件拷贝到输出目录
new MyPlugin()
]
}
使用前
使用后
webpack开发体验
一、原始流程
- 自动编译 watch工作模式 自动打包编译 yarn webpack --watch 监视文件变化后html不生成???
- 自动刷新浏览器 BrowserSync browser-sync output --files '**/*'
这两个搭配开发效率低
二、使用webpack-dev-server@3.8.2
yarn webpack-dev-server 运行 可待参数--open打开浏览器 具体参考 打包放在内存中,不会打包出来
webpack打包的都是可以访问,
devServer: {
// 字符串 数组
contentBase: './public'
},
// new CopyWebpackPlugin(['public']) // 文件拷贝到输出目录 开发阶段不使用
跨域请求问题:
devServer: {
// 字符串 数组
contentBase: './public',
proxy: {
'/api': {
// localhost:8080/api/users --> api.github.com/users
target: 'https://api.github.com',
pathRewrite: { '^/api': '' },
changeOrigin: true
}
}
},
sourceMap
调试和报错都是基于运行代码,打包后代码错误无法定位。
.js 最后一行,自动请求文件,逆向解析
配置devtool 打开sourceMap
devtool: 'source-map',
如何验证,webpack可以是一个对象,也可以是一个数组(可进行多次打包),eval使用eval执行, module不进行转换,inline 使用dataURL嵌入进去,hidden生成,但是不引入,nosource 没有源代码提供行列信息
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allMap = [ 'eval', 'cheap-eval-source-map', 'cheap-module-eval-source-map', 'eval-source-map', 'cheap-source-map', 'cheap-module-source-map', 'inline-cheap-source-map', 'inline-cheap-module-source-map', 'source-map', 'inline-source-map', 'hidden-source-map', 'nosources-source-map']
module.exports = allMap.map(item => ({
mode: 'none',
devtool: item,
entry: './src/index.js',
output: {
filename: `js/${item}.js`,
path: path.join(__dirname, 'output')
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
// new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}))
启动serve,output文件
选择合适的sourceMap:开发环境: cheat-module-eval-source-map 生产:none nosources-source-map
自动刷新问题HMR
比如页面有输入框,输入内容后,需要调整字体颜色,修改代码后,重新打包刷新,输入内容重新写
- 代码写死一些内容
- 额外代码实现刷新前保存,刷新后读取
- 页面不刷新前提下,模块可以及时更新
HMR Hot Module Replacement 模版热更新
开启热更新 集成在webpack-dev-server@3.9.0中
- 使用yarn webpack-dev-server --hot开启热更新,修改css会变化
- 使用webpack配置
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
devServer: {
hot: true //开启热更新
},
module: {
rules: [
// {
// test: /.md$/,
// use: ['html-loader', './markdown-loader']
// },
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 //10KB
}
}
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tmp.html'
}),
new CleanWebpackPlugin()
new webpack.HotModuleReplacementPlugin()
]
}
js文件更新刷新了页面,不可以开箱即用。
HMR API
处理文件更新的热替换
// index.js
import './input.js'
module.hot.accept('./input.js', () => {
console.log('打印***input更新')
// 此处应该为重新执行导入的内容提要
})
// input.js
console.log(392)
重新执行后结果,不会刷新页面
不同环境的配置
配置方式
- 配置文件根据环境不同导出不同配置
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// env 是webpack打包传入的 --env=production
module.exports = function (env, argv) {
const config = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
devServer: {
hot: true //开启热更新
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 //10KB
}
}
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tmp.html'
}),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
]
}
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['./public'])
]
}
return config
}
- 一个环境对应一个配置文件
使用webpack-merge@4.2.2 进行合并配置
//webpack.common.js
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// env 是webpack打包传入的 --env=production
module.exports = {
mode: 'none',
// 使用相对路径
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
devServer: {
hot: true //开启热更新
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.(jpeg|png|gif|webp|jpg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 //10KB
}
}
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tmp.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
//webpack.dev.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
const webpack = require('webpack')
// assign会覆盖对象 不会合并
module.exports = merge(common, {
mode: 'development',
devServer: {
hot: true //开启热更新
},
plugins: [new webpack.HotModuleReplacementPlugin()]
})
//webpack.prod.js
const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// assign会覆盖对象 不会合并
module.exports = merge(common, {
mode: 'production',
plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(['public'])]
})
运行命令使用yarn webpack --config webpack.dev/prod.js ,可以进行配置
优化配置
- DefinePlugin 代码注入全局成员 process.env.NODE_ENV常量 判断原型环境
const webpack = require('webpack')
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
plugins: [
new webpack.DefinePlugin({
// API_BASE_URL: 'https://api.example.com'
// API_BASE_URL: "'https://api.example.com'"
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
// index.js
console.log('打印***API_BASE_URL', API_BASE_URL)
不序列化API_BASE_URL: 'api.example.com'
序列化后使用API_BASE_URL: "'api.example.com'" 或者用JSON.stringfy
- Tree Shaking
无用代码去除 dead-code,生产模式自动开启。
// index.js
import { Button } from './components'
document.body.appendChild(Button())
// components.js
export const Button = () => {
return document.createElement('button')
console.log('打印***dead code')
}
export const Link = () => {
return document.createElement('a')
}
export const Heading = level => {
return document.createElement('h' + level)
}
none模式下打包结果
配置
optimization: {
// 去除未导入的模块
usedExports: true, // 标记枯树枝树叶
// 开启代码压缩,去除了dead code
minimize: true // 进行摇
concatenateModules:true // 将模块放在同一个函数中 提升运行效率,减少代码体积 scope Hoisting 作用域提升
}
- Tree Shaking & Babel
使用babel会失效 ,代码必须使用esm才能tree shaking,使用babel-loader可能会转换ESM-> CommonJS,
node_modules/babel-loader/lib/injectCaller.js
node_modules/@babel/preset-env/lib/index.js禁用了esm转换
强制开启
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', { modules: 'commonjs' }]]
}
}
},
- sideEffects 副作用
副作用:模块执行时,除了导出成员之外所做的事情
提供更大的压缩空间,一般用于npm包标记是否有副作用,生产环境默认开启
// src/components
/**
- heading.js
export default Heading = level => {
return document.createElement('h' + level)
}
- button.js
export default Button = () => {
console.log('打印***button')
return document.createElement('button')
console.log('打印***dead code')
}
- link.js
export default Link = () => {
return document.createElement('a')
}
- index.js
export { default as Button } from './button'
export { default as Heading } from './heading'
export { default as Link } from './link'
*/
// index.js
import { Button } from './components'
document.body.appendChild(Button())
// 需要开启副作用
optimization: {
sideEffects: true
}
// package.json
"sideEffects":false // 声明代码无副作用
前提:确定代码没有副作用
例如: 为什么还是打包出来???
// 新建一个extend.js
Number.prototype.pad = function (size) {
let ret = this + ''
while (ret.length < size) {
ret = '0' + ret
}
return ret
}
// 上述代码实际用到,但是标识了没有副作用,会影响运行
// index.js
import { Button } from './components'
import './src/extend'
document.body.appendChild(Button())
// package.json 中需要标识哪些文件没有副作用
"sideEffects": [
"./src/extend.js",
"*.css"
]
- Code Splitting 分包/代码分割
bundle体积很大,分包按需加载。http1.1 并发,浪费带宽。根据不同规则
- 多入口打包 webpack配置文件的entry属性配置类型:
字符串:一个入口文件
字符串数组:多个入口文件打包到一起,相当于一个入口打包
对象:多个入口文件分别打包
key:入口的名称 value:入口对应的文件路径
每个打包入口会形成一个独立的chunk,入口名称就是这个chunk的name(默认是main)。
一旦配置为多入口,输出的文件名也需要修改:
使用[name]占位符,动态输出文件名,[name]最终替换为入口的名称,即entry的key。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
// 使用[name]占位符,动态输出文件名
// [name]最终替换为入口的名称,即entry的key
filename: '[name].bundle.js',
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
}),
],
}
此时打包会输出两个html和两个bundle文件,但是发现html文件中将两个bundle文件全部引用了。 这是因为html-webpack-plugin插件会自动生成一个注入所有打包结果的html。 如果需要指定输出的html所使用的bundle,可以使用插件的chunks属性配置:
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
chunks: ['index'],
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
chunks: ['album'],
}),
每个打包入口,都会形成一个独立的chunk
。
而插件的chunks
属性,通过chunk
的名称,指定需要注入哪些chunk
。
提取公共模块 split chunks
optimization: {
splitChunks: {
// all 表示将所有的公共模块都提取到单独的bundle当中
chunks: 'all'
}
}
- 动态导入
使用import('xxx').then()进行动态
- 魔法注释 Magic Comments
默认通过动态导入产生的bundle文件,它的名称是一个序号,文件名为[number].bundle.js。
可以通过webpack特有的魔法注释,给它们定义名称。
具体使用就是,在import()的参数位置(前后都可以),添加一个特定格式的行内注释:
// 格式:/*webpackChunkName:'<name>'*/
import(/* webpackChunkName: 'posts' */'./posts/posts').then(() => {})
import('./posts/posts'/* webpackChunkName: 'album' */).then(() => {})
生成文件:
posts.bundle.js
album.bundle.js
album~posts.bundle.js 提供公共模块的文件也同步变化
如果多个模块使用的相同的chunkName,那它们最终会被打包到一起,自然不需要提取公共模块,最终只会生成一个文件。
借助这个特点,就可以根据情况,灵活组织动态导入的模块所输出的文件。
- 提取css文件 MiniCssExtractPlugin
mini-css-extract-plugin@0.8.0,不需要style-loader ,现在通过MiniCssExtractPlugin.loader进行link标签引入
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 通过 style 标签注入
MiniCssExtractPlugin.loader, // 通过 link 标签注入
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
- 压缩css代码 OptimizeCssAssetsWebpackPlugin
默认只针对js压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 通过 style 标签注入
MiniCssExtractPlugin.loader, // 通过 link 标签注入
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
打包后,就会在输出目录下,看到提取出来的css文件了,它的名称使用的是导入它的模块的名称(可能是魔法注释的名称,可能是合并打包成一个文件)。
打包效果:
css模块不会被包裹在函数中,作为数组参数的元素被使用。
而是在主入口文件执行方法中,以标签+文件路径的形式注入到html中。
建议:
如果样式内容不是很多的话,提取到单个文件的效果不是很好。
建议CSS文件超过150kb左右,才考虑提取到单个文件中。
否则css嵌入到代码中,减少一次请求,效果可能更好。
OptimizeCssAssetsWebpackPlugin 压缩输出的css文件
使用MiniCssExtractPlugin后,样式就被提取到单独的css文件中了。
前面说过,webpack在production模式下,会自动压缩优化打包的结果。
但是单独提取的css文件并没有被压缩。
这是因为webpack内置的压缩插件,仅仅支持JS文件的压缩。
对于其他类型的文件压缩,都需要额外的插件支持。
webpack推荐使用「optimize-css-assets-webpack-plugin」插件压缩样式文件。
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
mode: 'none',
output: {
filename: '[name].bundle.js',
},
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 通过 style 标签注入
MiniCssExtractPlugin.loader, // 通过 link 标签注入
'css-loader'
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
],
}
optimization.minimizer
webpack官方文档介绍时并不是将 「OptimizeCssAssetsWebpackPlugin」 插件配置在「plugins」数组中。
而是配置在 「optimization.minimizer」 数组中。
原因是:
配置在「plugins」中,webpack就会在启动时使用这个插件。
而配置在 「optimization.minimizer」 中,就只会在「optimization.minimize」这个特性开启时使用。
所以webpack推荐,像压缩类的插件,应该配置在「optimization.minimizer」数组中。
以便于通过「optimization.minimize」统一控制。(生产环境会默认开启minimize)
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
mode: 'none',
output: {
filename: '[name].bundle.js',
},
optimization: {
minimize: true,
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /.css$/,
use: [
// 'style-loader', // 通过 style 标签注入
MiniCssExtractPlugin.loader, // 通过 link 标签注入
'css-loader'
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
}),
new MiniCssExtractPlugin(),
// new OptimizeCssAssetsWebpackPlugin()
],
}
然而这样配置会导致JS不会被压缩。
原因是webpack认为,如果配置了minimizer,就表示开发者在自定以压缩插件。 内部的JS压缩器就会被覆盖掉。所以这里还需要手动将它添加回来。 webpack内部使用的JS压缩器是「terser-webpack-plugin」。 注意:手动添加需要安装这个插件才能使用。
// 只展示了添加的代码
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
// ...
}
- 输出文件名Hash. substitutions
一般部署前端的资源文件时,都会启用服务器的静态资源缓存。这样用户的客户端就可以缓存应用的静态资源,后续就不再需要重复请求服务器获取静态资源文件。从而整体提上了应用的响应速度。
不过开启服务器的静态资源缓存也有一些需要注意的地方:
如果在缓存策略中设置的失效时间过短,效果就不会特别明显。
如果设置的比较长,一旦这个应用发生了更新,重新部署过后,就没有办法及时更新到客户端。
为了解决这个问题,建议在生产环境中,在输出的文件名中添加哈希值(Hash)。
一旦资源文件发生改变,文件名称也会随之变化。
对于客户端而言,新的文件名就会发生新的请求,也就没有缓存,从而实现客户端及时更新。
这样就可以将缓存策略中的过期时间设置的非常长,而不用担心文件更新的问题。
webpack的filename属性,和绝大多数插件的filename属性,都支持通过占位符的方式为文件名设置hash。
不过它们支持3中hash,效果各不相同:
- [hash]:项目级别的hash 一旦项目中有任何改动,当前打包的hash就会发生变化
- [chunhash]:chunk级别的hash 打包时,只要是同一路的chunk,使用的hash就是一样的 动态导入的模块都会形成一个单独的chunk。这个chunk最终生成一个bundle(JS文件),如果配置了提取css文件,模块中引用的css也会被提取到css文件中。但它名义上仍然属于这个chunk。
例如:
通过动态导入方式会生成多个bundle,而这些JS模块中引入的css,如果被提取为css文件,使用的名称与JS模块一致,同样,它们使用的chunkhash也一样。而生成的这些bundle使用的chunkhash就不一样。
修改一个模块的内容,只会更新这些文件的chunkhash同一个chunk下的文件(js css)使用了这个模块的文件(因为模块名称变化,所以这个文件中引入这个模块的路径也发生了变化,相当于被动改变)相比[hash],[chunkhash]更精确一些
- [contenthash]:文件级别 根据输出文件的内容生成的hash即不同的文件拥有不同的hash 它影响到的只有:当前模块生成的文件使用这个模块的文件
注意:
如果配置了提取CSS文件,css实际上没有被包裹模块的bundle中,而是在主bundle文件的执行方法中,通过link方式注入到html中。所以此时修改css文件,只会更新自己和主bundle的hash,而不会影响引入它的子模块。
[contenthash]精确的定位到了文件级别的hash,只有当文件更新,才会更新它的文件名或路径。它最适合解决缓存问题。
hash长度默认20位,webpack允许通过在占位符用添加冒号+一个数字的方式指定hash的长度。
建议使用8位就够了:[contenthash:8]