ESM存在环境兼容问题
模块文件过多
网络请求频繁
所有的前端资源都需要模块化
综上所述,模块化是必要的
- 新特性代码编译
- 模块化JavaScript打包
- 支持不同类型的资源模块
模块打包工具
Webpack
- 模块打包器(Module bundler)解决模块化JavaScript代码打包的问题,可以将零散的模块代码统合到一个JS文件当中
- 模块加载器(Lodar)将环境兼容问题的代码进行编译转换
- 代码拆分(Code Splitting)将应用中所有的代码按照需求打包,不用担心代码打包到一起,比如应用加载过程中初次运行所必要的代码打包到一块,在运行过程中,实现增量加载,不用担心文件太大
- 资源模块(Asset Module)支持在JavaScript中以模块化的方式去载入任意类型的资源文件,比如在webpack当中,通过JavaScript import一个CSS文件,最终通过style标签的形式工作,其他文件类似如此
打包工具解决的是整个前端的模块化,并非单指JavaScript
webpack初体验
- 步骤1:
初始化项目
yarn init --yes
- 步骤2:
添加webpack依赖作为开发依赖
yarn add webpack webpack-cli --dev
- 步骤3:
检验webpack版本号
yarn webpack --version
- 步骤4:
使用webpack打包
yarn webpack
webpack会将文件打包为运行文件夹dist,并且将js代码压缩为main.js
- 如果觉得每次都用指令
yarn webpack
很麻烦的话,可以在package.json里用script注册build指令替换掉
再次进行打包就是
yarn build
webpack 配置文件
webpack 4以后的版本支持0配置的方式直接启动打包,整个打包过程会按照约定,将src/index.js作为入口,存入dist/main.js中
- 自定义打包配置
项目根目录创建webpack.config.js的文件。这个文件是运行在node环境中的JS文件,需要按照commonJS的方法编写代码
const path = require('path')
module.exports = {
// 指定webpack打包入口文件的路径 ./是不能省略的
entry: './src/main.js',
// 设置输出文件的位置,要求是对象,通过对象的filename,指定输出名称,
output: {
filename: 'bundle.js',
// 指定输出文件所在的目录,path必须要是绝对路径,载入path模块,获得路径
path: path.join(__dirname, 'output')
}
}
webpack工作模式
不同环境的几组预设配置
默认的工作模式的production
修改工作模式的指令就是在运行webpack打包时的指令加上mode,例如
yarn webpack --mode 工作模式
工作模式分为三种:
- production 生产模式,webpack会自动优化打包结果,这也是默认的工作模式
- development 开发模式,webpack会自动优化打包的速度,添加一些调试过程中的辅助
- None模式,webpack就是运行最原始的打包,不作任何的额外处理
差异可以在官方文档找到:
也可以在配置文件里设置工作模式
-
在webpack.config.js文件里声明mode属性的属性值,就无需配置指令的参数了
webpack打包结果运行原理
小知识:VScode快捷折叠代码的快捷键是ctrl+k+ctrl+0
将所有的模块放入一个文件,并且构造成为一个立即执行函数,可以通过打断点的方式进行一步步调试,将各个模块通过互相调用联系起来
webpack 资源模块加载
通过webpack引入前端项目中的任意文件
- webpack内部默认只处理JavaScript文件
- 要想让webpack处理其他类型的文件例如css等等就需要新的loader
- CSS需要的新loader有:
- css-loader
- style-loader
yarn add style-loader --dev
yarm add css-style --dev
另外安装了loader之后还需要在webpack.config.js文件里进行设置
设置module下的rules
test:表示的是匹配打包过程中的文件路径
use:匹配到的文件需要使用的loader,多个loader的话执行顺序是从后往前执行
Loader是webpack的核心特性
通过不同的Loader可以实现加载任何类型的资源
webpack 导入资源模块
打包入口=>运行入口
JavaScript驱动整个前端应用的业务
例如:
因为是单独的类名,所以还需要使用element.classList.add来添加标签的类名实现选择器
- 逻辑合理,JS确实需要资源文件
- 保证上线资源不缺失,都是必要的
webpack 文件资源加载器
导入一个png资源
这个时候同样是需要新的Loader,因为导入了webpack默认不能识别的资源类型
yarn add file-loader --dev
同样,需要在webpack配置文件设置
webpack会默认的认为打包的内容会放在网站的根目录下面,可能会造成路径问题
更改问题:
通过配置文件告知webpack
publicPath: 'dist/'
publicPath是默认空字符串的,所以要记得修改
webpack Data URLs 与 url-loader
-
特殊的URL协议,当前的url就可以直接表示文件内容的方式
图片或者是字体这一类无法通过文本表示的二进制文件,就可以通过base64编码结果为字符串来标示内容
yarn add url-loader --dev
然后将rules下的图片类改成url-loader
loader: 'url-loader',
url-load 适合小文件的使用,小文件使用Data URLs,可以减少请求次数。大文件单独提取存放,使用file-loader,提高加载速度
- use作为类,loader指定url-loader,options的limit则是指定范围大小内的文件进行url处理,之外的进行file-loader处理,前提是必须要添加file-loader的依赖
use: {
loader: 'url-loader',
// 添加配置选项
options: {
// 只将10KB以下的文件进行url-loader的处理,以上的依然使用file-loader
limit: 10 * 1024 // 10 KB
}
}
webpack 常用加载器分类
- 编译转换类
加载到的资源模块转换为JavaScript代码,比如css-loader - 文件操作类
加载到的资源模块拷贝到输出目录,导出访问路径,比如file-loader -
代码检查类
统一代码风格,提高代码质量,比如eslint-loader
webpack 处理ES2015
因为模块打包需要,所以处理import,export,并不能处理ES6的其他特性
如果需要webpack打包过程中同时处理其他的ES6特性的转换,需要为JS文件配置一个额外的编译器loader。比如babel-loader
yarn add babel-loader @babel/core @babel/preset-env --dev
修改webpack.config.js文件
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
- webpack只是打包工具
- 加载器可以用来转换编译代码
webpack 模块加载方式
- 遵循ESM 标准的import声明
- 遵循CommonJS标准的require函数
- 遵循AMD标准的define函数和require函数
不要混合使用,不方便统一标准的实施 - Loader加载的非JavaScript也会触发资源加载
-
样式代码中的@import属性和url函数
-
HTML代码中图片标签的src属性
-
webpack核心工作原理
loader机制是webpack的核心
先找到一个.js文件作为入口,就犹如树状一样,每个支点找到对应的资源文件,最后整合到一起写入bundle.js文件
webpack Loader的工作原理
loader就是负责资源文件从输入到输出的转换
对于同一个资源可以依次使用多个loader
比如:
css-loader->style-loader
webpack 插件机制
增强webpack自动化能力
Loader专注实现资源模块加载
Plugin解决其他自动化工作
比如:清除dist目录,拷贝静态文件到输出目录,压缩输出代码
webpack 自动清除输出目录插件
yarn add clean-webpack-plugin --dev
使用
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin()
]
webpack 自动生成HTML插件
通过webpack输出HTML文件
插件:html-webpack-plugin
不需要解构:
const HtmlWebpackPlugin = require('html-webpack-plugin')
与上述自动清除目录插件的导入与使用基本是一致的
生成模板,以供webpack生成html参照结构
同时输出多个页面文件
以后如果要添加多个HTML文件就写多个实例对象就可以了
webpack常用插件使用总结©-webpack-plugin
以下这个插件最好不要在开发阶段使用,一般都是留在上线前的那一次打包中使用
- copy-webpack-plugin的作用就是将指定文件夹下面的文件拷贝到输出目录
用法与导入导出都与上述一致,不需要赘述
之后在 官方说明多看看特别用法,社区还提供了更多的插件
需求->关键词->搜索
webpack 开发一个插件
相比于loader,plugin拥有更宽的能力范围
plugin通过钩子机制实现
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// console.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 开发体验的设想
理想的开发环境
- 以HTTP server 运行
- 自动编译+自动刷新
- 提供Source Map支持
实现自动编译
监听文件变化,自动重新打包
启动命令后面加个watch参数,启动监听模式
yarn webpack --watch
自动刷新浏览器
browserSync工具,以前有过介绍
browser-sync dist --files "**/*"
弊端:
操作麻烦,要开两个终端,效率上降低了
webpack Dev Server
集成了自动编译和自动刷新浏览器等功能
yarn add webpack-dev-server --dev
运行
yarn webpack-dev-server
打包结果不会存放在磁盘上,而是存放在内存当中,所以不会有dist文件夹,可以减少磁盘读写操作,大大提高效率
自动唤醒浏览器
yarn webpack-dev-server --open
webpack Dev Server静态资源访问
Dev Server默认只会serve打包输出文件
只要是webpack输出的文件,都可被直接访问到
静态资源文件也需要serve
devServer: {
// 指定额外的静态资源路径,可以为字符串或者数组,也就是一个或者多个
contentBase: './public',
}
webpack Dev Server 代理API
并不是任何情况下API都应该支持CORS同源策略
如果是同源部署,根本没必要开启CORS
出现问题:开发阶段接口跨域问题
解决问题:配置代理,把接口服务代理到本地的开发地址
webpack Dev Server支持配置代理
目标:将Github的API代理到本地开发服务器
在devServer当中添加一个proxy属性,这个属性就是添加代理服务配置的
devServer: {
proxy: {
// 请求路径前缀
'/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 的主机名
// 默认使用的就是用户的localhost:8080
// changeOrigin:true就会以实际代理的主机名去请求,就是api.github.com
changeOrigin: true
}
}
},
Source Map
运行代码和源代码之间完全不同,如果需要调试应用,错误信息没法定位,调试或者报错都是基于运行代码,Source Map就是解决这类问题的办法(源代码地图)
用于映射源代码和转换代码之间的关系
version属性:记录Source Map版本号
sources属性:转换之前源文件的名称,可能是多个文件,所以是数组
name属性:源代码成员名称
mapping属性:转换之后的代码的字符与转换之前对应的映射关系
最新版jQuery已经去除了引入source map的注释
source map 解决了源代码和运行代码不一致所产生的问题
webpack 配置 Source Map
在webpack配置文件里有一个属性叫做devtool
,配置开发过程中的辅助工具
webpack 支持12种不同的方式
每种方式的效率和效果不同
eval模式下的Source Map
这种模式下不会生成source-map文件,只能定位源代码名称,不知道行列信息
不同devtool之间的差异
所有的不同模式下的devtool测试:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'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 = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
// module.exports = [
// {
// entry: './src/main.js',
// output: {
// filename: 'a.js'
// }
// },
// {
// entry: './src/main.js',
// output: {
// filename: 'b.js'
// }
// }
// ]
运行之后可以慢慢观察各个模式的差距
- eval:是否使用eval执行模块代码
- cheap:Source Map是否包含行信息
- module:是否能够得到Loader处理之前的源代码
- inline:使用dataURL的方式嵌入到url当中(最少用到的)
- hidden:看不到source-map的效果,但是确实生成了map文件,跟jquery是一样的,代码中并没有引入,开发第三方包比较有用
- nosources:能看到错误出现的位置,但是点击错误信息,看不到源代码的,可以找到错误,确保源代码不会被暴露的情况
webpack选择合适的 Source Map模式
来自师傅的个人开发经验之谈:
选择cheap-module-eval-source-map
- 代码每行不超过80个字符(定位到行就可以了)
- 经过Loader转换过后的差异较大,频繁使用框架(需要查阅转换之前的代码)
- 首次打包速度比较慢,但是重写打包相对较快
生产环境下
选择None
- Source Map会暴露源代码
- 调试是开发阶段的事情,在开发阶段尽可能找到所有的bug
再不济也要选择nosources-source-map - 不要暴露源代码的内容
没有选择的差异,理解不同模式的差异,适配不同的环境
webpack 自动刷新的问题
比如我测试一个文本输入页面,一旦有变化,文本会丢失,又要重新输入
页面不刷新的前提下,模块也可以及时更新
webpack HMR
HMR又名模块热替换(热更新)
应用程序运行过程中实时替换掉某个模块,运行状态不会改变,以解决自动刷新导致页面内容丢失的问题。
热替换只是将修改的模块实时替换到应用中
HMR是webpack中最强大的功能之一
已经集成到了webpack-dev-server中,不必要专门添加什么依赖
命令:
webpack-dev-server --hot
相对应的也有可以在配置文件中添加相应的配置打开HMR
devServer: {
hot: true
},
然后载入webpack模块
const webpack = require('webpack')
再在plugins里写入实例
new webpack.HotModuleReplacementPlugin()
HMR还需要额外的操作才能正常工作
webpack中的HMR需要手动处理模块热替换逻辑
样式文件热更新开箱即用的原理:
经过loader处理了,style-loader中已经自动处理了热更新
在框架下的开发,每种文件都是有规律的。所以JS就可以自动热更新,通过脚手架创建的项目内部都集成了HMR方案
HMR APIs
// ============ 以下用于处理 HMR,与业务代码无关 ============
// console.log(createEditor)
if (module.hot) {
let lastEditor = editor
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
// console.log(createEditor)
const value = lastEditor.innerHTML
document.body.removeChild(lastEditor)
const newEditor = createEditor()
newEditor.innerHTML = value
document.body.appendChild(newEditor)
lastEditor = newEditor
})
module.hot.accept('./better.png', () => {
img.src = background
console.log(background)
})
}
HMR注意事项
- 处理HMR的代码报错会导致自动刷新
解决方法:
hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
- 没启用HMR的情况下,HMR API报错
缺失:
new webpack.HotModuleReplacementPlugin()
解决:
添加控制条件
if(module.hot)
- 代码中多了一些与业务无关的代码
移除了热更新的实例与导入代码,业务模块的相关代码会自动移除
webpack 生产环境优化
生产环境和开发环境有很大的差异
生产环境注重运行效率
开发环境注重开发效率
webpack 提供模式(mode) 为不同的工作环境创建不同的配置
webpack不同环境下的配置
- 配置文件根据环境不同导出不同配置
(中小型项目) - 一个环境对应一个配置文件
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
// 判断什么模式
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
-
不同环境的配置文件
common.js:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
使用什么模式,就用这条指令:
yarn webpack --config 哪一个文件
webpack DefinePlugin
为代码注入全局成员
这是webpack内置的插件
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
webpack Tree-shaking
- [摇掉]代码中没有引用的部分,未引用代码(dead-code)
- Tree-shaking会在生产模式production下自动开启
- Tree-shaking不是某一个配置选项
- 非生产模式下的配置:
// 集中配置webpack内部的优化功能
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: true
}
webpack Tree-shaking 与 babel
- Tree Shaking的前提是ESM
- 由webpack打包的代码必须使用ESM
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// ['@babel/preset-env', { modules: false }]
// 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
Babel并不会让Tree-shaking失效
webpack sideEffcts(副作用)
副作用:模块执行时除了导出成员之外所作的事情
一般用于npm包标记是否有副作用
optimization: {
sideEffects: true,
}
确保你的代码真的没有副作用,否则会误删
-
样式文件属于副作用模块
Code Splitting(代码分割)
所有的代码都会被打包到一起,bundle体积过大
并不是每一个模块在启动的时候都是必要的
分包,按需加载
-
多入口打包(多页面应用程序)
一个页面去对应一个打包入口
公共部分单独提取
提取公共模块
- ESM动态导入
按需加载:需要用到某个模块时,再加载这个模块
动态导入的模块会被自动分包(更为灵活)
魔法注释
灵活组织动态加载的模块输出的文件
MinCssExtraCtPlugin
提取CSS到单个文件
yarn add mini-css-extract-plugin --dev
在webpack配置文件中导入,再通过实例引用
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
new MiniCssExtractPlugin()
会将样式的CSS代码单独提取到一个文件中,再引用,也就是说不需要style-loader了,取而代之的是
MiniCssExtractPlugin.loader
如果css文件体积大于150kb了,就考虑提取到单独文件当中
OptimizeCssAssetsWebpackPlugin
压缩输出的CSS文件
webpack内置的压缩插件只针对JS的压缩,CSS没有压缩,需要额外的插件
yarn add optimize-css-assets-webpack-plugin --dev
运行原理照旧,有一点需要注意:配置在optimization的minimizer属性当中
但是有一个缺点,如果配置了minimizer,编辑器就会自动认为要使用配置的压缩规则,那么JS就不会被压缩了
输出文件名Hash
生产模式下,文件名使用Hash值
一旦资源发生改变,文件名也会发生改变,对于客户端来说全新的文件名就是全新的请求,就没有缓存的问题,不用担心文件更新之后的问题
webpack中的filename属性和绝大多数插件的filename属性都支持通过占位符的方式为文件名设置Hash
- [name]-[hash]:项目级别,有任何一个地方发生变化,hash值就全部发生变化了
- [name]-[chunkhash]:同一路的打包,chunkhash都是同样的hash,在同一路下的任意文件作出改动,其他的同一路文件都会发生变化
-
[name]-[contenthash]:文件级别的hash
(解决缓存问题最好的方式)
可以通过:number的方式来指定hash长度