webpack是一个JavaScript应用程序的静态模块化打包器,意味着webpack在不进行特殊配置的情况下就只认识JavaScript一种语言,只能处理JavaScript这一种语言
1)原理&作用
0) 理解前端模块化
⇒ 为了避免变量冲突和覆盖,不同js文件间可以使用命名空间,譬如在susan.js中
var susan = {
name: 'susan',
tell: function(){
console.log('我的名字:', name)
}
}
在liming.js中
var liming = {
name: 'liming',
tell: function(){
console.log('我的名字:', name)
}
}
⇒ 但是上述办法无法保护变量,变量可以被随意访问和篡改,为了避免变量被随意访问和篡改,可以利用函数自调用的办法,利用闭包,即内层函数引用上层函数的变量,内层函数被return后依然引用着外层函数的变量的特性
var susanModule = (function(){
var name = 'susan'
return {
tell: function(){
console.log('我的名字:', name)
}
}
})()
此时我们为tell方法绑定了一个专属变量name,同时又保护了这个变量不被污染(因为通过susan.tell()可以顺利执行,但是susan.name为undefined)
如上所述,这样一个被保护起来的作用域范围,则成为模块作用域,通过像这样形成模块作用域,来隐藏该隐藏的变量,暴露该暴露的变量,既保证关键变量不被随意更改,同时又让逻辑可重用
⇒ 更加标准和通用的写法如下
(function(window){
var name = 'susan'
function tell(){
console.log('我的名字:', name)
}
window.susanModule = {tell}
})(window)
即把要暴露的函数挂载到window上,通过susanModule.tell()即可访问执行
所以前端模块化,其实就是在利用作用域链、闭包这些特性,来解决保护变量同时重用逻辑的问题的方案 ,它的优点在于:
- 对作用域进行了封装
用模块把代码封装起来,保证模块内部的实现不会暴露在危险的全局作用域中,只需要将模块的功能通过接口的方式(如susanModule.tell())暴露出去给其他模块调用即可,从而避免了污染全局命名空间的问题 - 保证了模块的复用性,包括vue中的组件化也是同样的道理
- 解除耦合
可以帮助快速的定位问题,定位问题时我们只需要去关注对应的模块,从而提升系统的可维护性
站在软件工程的角度去理解模块化
另:在立即执行函数方法后,模块化方案又经历了以下3个阶段:
-
AMD(Asynchronous Module Definition 异步模块定义)
-
COMMONJS
-
ES6MODULE
JS这门语言本身不具备模块化的特性,ES6开始,模块化才开始有原生语法的支持
1) webpack的打包机制
- webpack与立即执行函数的关系
- webpack打包的核心逻辑
2)webpack的打包过程
即webpack在打包的时候都做了什么,经历了哪些流程
当我们在根目录下运行webpack,webpack会去src目录下寻找index.js文件并将其打包为main.js,放入专门放打包结果的文件夹dist
webpack的打包过程
- 从默认的入口文件
src/index.js
开始,分析整个应用的依赖树
从默认的入口文件src/index.js
开始,遍历src/index.js
中所引入的模块及src/index.js
本身的逻辑,放入默认的出口文件dist/main.js
文件中 - 将每个依赖模块包装起来,放到一个数组中等待调用
- 实现模块加载的方法,并把它放到模块执行的环境中,确保模块间可以互相调用
- 把执行入口文件的逻辑放在一个函数表达式中,并立即执行这个函数
4) 为什么要打包:
- 一般项目中js文件数目很大且相互之间有依赖关系,如果不打包,直接让这些文件保持分离状态,则我们需要关心加载顺序
- 如果不打包,这些js文件处于分离状态,我们需要分开请求加载,则消耗性能
所以通过打包,将这些文件打包为一个dist/main.js
文件,则可以帮助我们解决上述问题
2) webpack的核心特性
1.webpack-dev-server 以及 热更新/热替换
① 实现热更新
热更新(HMR)即在启动本地服务后,不刷新浏览器的情况下,实现文件改动的同步更新,实现办法如下
首先,配置webpack.congif.js参数如下
const path = require('path')
// 实现热更新 -0: 引入webpack
const webpack = require('webpack')
module.exports = {
entry: path.resolve(__dirname, 'src/index.jsx'),
// plugins
plugins: [
// 实现热更新 -1:配置插件
new webpack.HotModuleReplacementPlugin()
],
devServer:{
hot: true, // 热更新 -2:配置devServer参数
},
}
然后,配置入口文件
import App from './App.jsx'
// 实现热更新 -3
if(module.hot){ // 如果module.hot为true,则赋予热更新的能力
module.hot.accept(error=>{
if(error){
console.log('热替换出bug了')
}
})
}
最终,在启动服务后即可实现热更新功能
② 关于启动本地服务
- 通过运行
webpack-dev-server
命令,原地启动一个本地服务,可以监听工程文件的改动,当修改文件并保存时,可以动态的实现实时打包,自动刷新浏览器 - 如果是webpack5,命令行输入
webpack serve
即可 webpack server --config webpack.config.js
可以指定执行的webpack配置文件webpack serve --open
,启动服务同时打开浏览器
注1:如果运行webpack-dev-server
命令后报错提示command not found
,是因为没有全局安装webpack-dev-server,可以通过npm install webpack-dev-server -g
安装后再执行命令,或者直接运行node_modules/.bin/webpack-dev-server中的可执行文件,即命令行直接输入./node_modules/.bin/webpack-dev-server
以上命令行可以在package.json的scripts中自定义,如下所示
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": { // 自定义脚本
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode production", // 打包
"start": "webpack serve --mode development --open" // 启动服务并打开浏览器
},
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^5.17.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.11.2"
}
}
注2:如果命令行运行webpack
报错,很可能是因为没有全局安装webpack-cli,只需要npm install webpack-cli -g
即可
2.loader(翻译官)
loader是对webpack能力的拓展,webpack本身只能处理js文件,其他类型譬如css、html文件则需要通过loader进行处理
loader本质是一个文件加载器,实现文件的转译编译功能
即把浏览器看不懂的代码翻译成浏览器看得懂的代码,譬如把Chrome看不懂的TS代码翻译为浏览器看得懂的JS代码
loader用于处理资源文件,接受资源文件为一个参数,处理完后返回一个新的资源
注:loader独立于webpack,webpack内部不包含loader,所以必须手动安装
- 使用babel-loader转化ES6代码,转换为浏览器可以直接运行的js代码
npm install --save-dev babel-loader babel-core
常用loader:
- 使用css-loader处理css文件,再经过style-loader生成style标签并插入HTML页面中
- 使用postcss-loader以及它的插件autoprefixer为
display: flex;
添加浏览器前缀 - 使用less-loader处理less文件
- 使用sass-loader处理less文件
- 使用html-loader处理html文件
- file-loader处理项目中引入的图片文件或者其他文件,可以将项目中以’相对路径’引用的文件,处理为以’绝对路径’引用
- url-loader,当URL引用的图片或文件小于limit时,可以转换生成一段base64编码被打包进js或者html里面,而不再是一个url,不再需要http请求去load文件,当图片或文件大于limit时,则转交file-loader处理
1) file-loader
对于图片文件 使用file-loader处理,可以将项目中以’相对路径’引用的文件,处理为以’绝对路径’引用
- 对于 根目录下index.html中直接以相对路径引用的图片,以及 css文件中直接以相对路径引用的图片,使用file-loader处理
使用项目中以相对路径引用的图片 - 对于模板文件中直接以相对路径引用的图片,使用如下写法,再使用file-loader处理
<img src="${ require('../../assets/bg.png') }"/>
2)url-loader
当URL引用的图片或文件小于limit时,可以转换生成一段base64编码被打包进js或者html里面,而不再是一个url,不再需要http请求去load文件,当图片或文件大于limit时,则转交file-loader处理
3) 区别
- file-loader通过http请求load的图片浏览器可以进行缓存,当图片重复性非常高的时候,可以使用file-loader
- 当图片或者文件多次被引用时,相同的一段base64编码可能存在项目的很多地方,从而导致打包后的代码冗余;但是可以减少http请求次数
3. plugin(插件)
也是对webpack功能的增强,和loader的区别在于,plugin强调时间监听的能力,plugin可以在webpack内部监听一些事件,并且改变一些文件打包后的输出结果
plugin往往都是以构造函数的形式存在,所以使用之前需要引入,引入后才能new
常用plugin
plugins处理loaders无法完成的功能。
- CommonsChunkPlugin,将chunks相同的模块代码提取成公共js
- CleanWebpackPlugin,清理构建目录
- ExtractTextWebpackPlugin,将CSS从 bunlde 文件里提取成一个独立的CSS文件
- CopyWebpackPlugin,将文件或者文件夹拷贝到构建的输出目录
- HtmlWebpackPlugin,创建html文件去承载输出的bundle
- UglifyjsWebpackPlugin,压缩 JS
- ZipWebpackPlugin,将打包出的资源生成一个zip包
4.webpack.config.js(webpack配置文件)
webpack通过webpack.config.js进行webpack打包时的自定义配置
一个webpack.config.js样板(webpack4/5)
const path = require('path') // 在处理文件路径时需要用到的模块
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
// - 指定入口文件,即工程资源的入口,默认值=index.js
// - 可以是一个文件,也可以是多个文件,webpack会以入口文件为切入口,遍历依赖进行打包
// - 可以理解为一个依赖树的根节点
entry: './app.js',
// - 指定出口文件
// - 每一个入口都会对应一个最终的打包结果
output: {
// -- 指定出口文件所在路径,必须是绝对路径,默认值=dist/main.js
path: path.join(__dirname, 'dist'), // __dirname指当前目录,dist指相对路径,作为参数传入path.jion()中得到绝对路径
// -- 指定出口文件名称
filename: 'bundle.js'
},
// - 配置devServer,针对npm run dev 或者 webpack-dev-server后启动的本地服务进行配置
// 执行webpack-dev-server启动本地服务后打包并不会在dist目录生成实际的文件,可以理解为打包后的资源只存在于内存中,当浏览器发生请求时会直接从内存中加载打包后的资源文件
devServer: {
// -- 指定服务端口,默认值=8080
port: 3000,
// -- 指定打包后的资源路径
publicPath: '/dist'
},
// - loader, loader本质是一个文件加载器,实现文件的转译编译功能
module: {
rules: [
{
test: /\.css$/, // 对于.css结尾的文件
exclude: /node_modules/, // 排除node_modules,不对其进行处理
use: [ // 使用指定的loader 执行顺序 下 → 上,所以要先用css-loader解析,然后用style-loader生成style标签插入页面
'style-loader', // 为解析后的样式生成style标签并插入页面中 npm install style-loader --save-dev
'css-loader', // 使用css-loader解决css语法解析的问题 需 npm install css-loader --save-dev
]
}
]
},
// 插件 配置前需要npm install uglifyjs-webpack-plugin --save-dev,并且使用import引入
plugins: [
// 减小整个项目的代码体积
new UglifyJSPlugin(),
// 生成打包后的html
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'), // 指定需要被处理的文件的路径
filename: 'index.html', // 指定打包后的文件名
}),
],
// resolve
resolve: {
// 向webpack声明以下后缀在进行引入时不再写明
// 在webpack工作时,如果在文件中读到类似 import App from './App' 的代码,则会遍历下方数组中的后缀,验证和APP拼接后是否能在目录下找到对应文件
extensions: ['.wasm','.mjs','.js','.jsx','.json']
}
}
3) 优化webpack性能(包括优化构建性能、优化构建后的结果)
1.打包结果优化(空间维度)
即尽可能减小打包后的文件的体积,加快传输速度,优化用户体验
- 本身webpack可以实现压缩功能,在指定压缩模式为生产模式,即
webpack --mode production
时,webpack会自动压缩 - 通过TerserPlugin自定义优化配置,webpack.config.js配置如下
const TerserPlugin = require('terser-webpack-plugin') // 引入插件
module.exports = {
// 用于存放有关压缩的配置
optimization: {
mininizer: [
// 使用插件
// UglifyJSPlugin 的升级版,UglifyJSPlugin在es5方面比较优秀,UglifyJSPlugin-es对es6做了强化,后续未再更新,TerserPlugin是UglifyJSPlugin-es重新拉取的分支
new TerserPlugin({
// 使用缓存,加快构建速度
cache: true,
// 开启多线程,加快打包速度
parallel: true,
terserOptions: {
// 压缩体积
compress: {
unused: true, // 自动剔除无用代码
drop_debugger: true, // 去掉开启的debugger
drop_console: true, // 去掉console的代码
dead_code: true, // 移除无用代码
}
}
})
]
},
}
2.构建过程优化(时间维度)
即尽可能加快构建速度,提升开发者的效率
- 节省解析时间
// webpack.config.js
mudule.exports = {
module: {
noParse: /node_module\/(jquery\.js)/, // 节省解析时间
}
}
- 节省查找时间
exclude排除node_modules,不对其进行处理,或者使用include指定要处理的文件
exclude优先级高于include和test
// webpack.config.js
mudule.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/, // 排除node_modules,不对其进行处理
// 或者使用include指定要处理的文件
use: {
loader: 'babel-loader',
}
}
}
],
}
}
- 利用多线程提升构建速度
node是单线程,而webpack运行在node环境下,所以webpack本身是单线程的,利用HappyPack可以实现多进程程,将任务分解为多个子进程,并发执行,如下所示
// webpack.config.js
const HappyPack = require('happypack')
// 根据CPU数量创建线程池(利用进程实现多线程)
const happyThreadPool = HappyPack.happyThreadPool({size:OscillatorNode.cpus().length})
module.exports = {
plugins: [
new HappyPack({
id: 'jsx',
threads: happyThreadPool,
loaders: ['babel-loader'] // 给loader配置线程
// 配置时要注意loader是否支持,如url-loader file-loader 则不支持happypack
})
],
}
- 利用多线程提升构建速度
或者使用thread-loader针对loader进行优化,将loader放入线程池,设置时需放在所有loader之前,如下所示
// webpack.config.js
mudule.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader'
// 后面再增加其他loader
]
},
],
}
}
3.Tree-Shaking(优化思想)——webpack本身的优化特性
本质是消除无用的js代码(DCE),webpack做的事情有2件:
分析es6的引入情况,去除不实用的import引入
4.评价打包性能的可视化工具:webpack bundle analyzer
本身其实是一个插件(plugin)
① 安装
npm install webpack-bundle-analyzer
② 配置webpack.config.js
const BundleAnalyZerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin // 引入插件
module.exports = {
// plugins
plugins: [
// BundleAnalyZerPlugin
new BundleAnalyZerPlugin()
],
}
③ 打包,查看效果
npm run build
打包后生成打包后文件同时webpack启动一个服务展示对应图示
打包时排除node_modules
node_modules是已经被打包过的文件,不需要再用babel进行处理打包,所以排除node_modules下的文件,可以提高打包速度,
module.exports = {
entry: './src/app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].bundle.js',
publicPath: 'http://cdn.com'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['latest']
}
},
// node_modules是已经被打包过的文件,不需要再用babel进行处理打包
// 所以排除node_modules下的文件,可以提高打包速度
exclude: path.resolve(__dirname, './node_modules/'),
// include可以用来指定包含的范围
include: path.resolve(__dirname, './src/'),
},
],
},
// 插件
plugins: [
// ...
],
}
4) 其他:
webpack.config.js(webpack配置文件)
const path = require('path');
const htmlWebpckPlugin = require('html-webpack-plugin')
module.exports = {
// 指定上下文
context: __dirname,
// 指定入口文件
entry: './src/app.js',
// 指定出口文件
output: {
// 规定出口文件所在目录,使用node的API,path.resolve(),通过’当前路径‘+’相对路径‘解析得到绝对路径
path: path.resolve(__dirname, 'dist'), // __dirname:当前路径,'dist': 相对路径
// 规定出口文件的名称
filename: 'js/[name].bundle.js',
// 指定出口文件所在地址
// publicPath: 'http://cdn.com'
},
module: {
// 指定loader
rules: [ // 数组中的每一项,都是一个规则
// - 对于.js结尾的文件 使用babel-loader
{
test: /\.js$/,
use: {
loader: 'babel-loader',
// 需要npm install --save-dev babel-loader babel-core
options: {
presets: ['latest'] // 指定插件转换特定的es特性
// 需要npm install --save-dev babel-preset-latest
}
},
// node_modules是已经被打包过的文件,不需要再用babel进行处理打包,
// 所以排除node_modules下的文件,可以提高打包速度
exclude: path.resolve(__dirname, 'node_modules'),
// include可以用来指定包含的范围
include: path.resolve(__dirname, 'src'),
},
// - 对于.css结尾的文件 使用css-loader处理,再经过style-loader,生成style标签并插入HTML中
{
test: /\.css$/,
use: {
loader: 'style-loader!css-loader', // loader的处理方式:右 → 左
// 需要npm install --save-dev css-loader style-loader
}
},
// - 对于.less结尾的文件 使用less-loader处理,再经过css-loader和style-loader,生成style标签并插入HTML中
{
test: /\.less$/,
use: {
loader: 'style-loader!css-loader!less-loader', // 也可以去除'-loader'
// 需要npm install --save-dev less-loader
}
},
// - 对于.sass结尾的文件 使用sass-loader处理,再经过css-loader和style-loader,生成style标签并插入HTML中
{
test: /\.sass$/,
use: {
loader: 'style-loader!css-loader!sass-loader',
// 需要npm install --save-dev sass-loader
}
},
// - 对于.html结尾的文件 使用html-loader处理
{
test: /\.html$/,
use: {
loader: 'html-loader'
// 需要npm install --save-dev html-loader
},
},
// - 对于图片文件 使用file-loader处理,可以将项目中以'相对路径'引用的文件,处理为以'绝对路径'引用
{
test: /\.(png|jpg|gif|svg)$/i,
use: {
loader: 'file-loader',
// 需要npm install --save-dev file-loader
options: {
// 指定处理后的图片的文件名,[name]为文件名占位符,[ext]为文件名后缀占位符
// 如果写为'images/[name].[ext]',还可以指定文件所在的文件夹
name: '[name].[ext]',
// 处理后的图片文件默认放在dist目录下
// 指定处理后的图片的地址
outputPath: 'images',
}
},
},
// -当URL引用的图片或文件小于limit时,可以转换生成一段base64编码,而不再是一个url,
// 当图片或文件大于limit时,则转交file-loader处理
{
test: /\.(png|jpg|gif|svg)$/i,
use: {
loader: 'url-loader',
// 需要npm install --save-dev url-loader
options: {
// 设置limit为 20kB
limit: 20000,
name: '[name].[ext]',
}
}
}
],
},
// 插件
plugins: [
// 自动生成html页面
new htmlWebpckPlugin({
// 即将生成html的文件名
filename: 'index.html',
// 以根目录下的index.html为模板生成HTML页面
template: 'index.html',
// js文件要注入在head里还是body里,或者可以设置为 false
inject: 'body',
// <title><%= htmlWebpackPlugin.options.title %></title> 通过上述设置在模板index.html中设置title
title: 'webpack is good',
// 对html文件进行压缩
minify: {
// 删除模板html文件中的注释
removeComments: false,
// 删除模板html文件中的空格
collapseWhitespace: false,
},
// 指定要注入的chunks
// chunks: ['main', 'a'],
// 指定要排除的chunks
// excludeChunks: ['c'],
// 还可以自定义属性,譬如eee
eee: new Date()
}),
],
}
新建一个webpack项目
新建一个文件夹
mkdir demo
进入文件夹
cd demo
初始化npm
npm init -y
安装webpack和webpack-cli
npm install webpack webpack-cli -d
为了能直接执行命令webpack,全局安装webpack和webpack-cli
npm install webpack webpack-cli -g
新建src文件夹