webpack工程化
概念
本质:通过编译打包是项目开发版和上线版完全分离
原理:模块打包,把模块接收并进行处理,最后生成几个静态资源文件,最后项目引入资源文件
强大的功能:懒加载,代码分割,Tree Shaking
webpack打包工具模块:打包过程中代码转换与项目代码与结构优化的一系列工具集合
uglifyjs-webpack-plugin: 压缩混淆JS插件html-webpack-plugin:压缩HTML注入脚本插件autoprefixer:自动添加CSS兼容性前缀插件clean-webpack-plugin:清除多余文件插件babel-loader/babel-core/babel-preset-latest:ES6脚本代码转换工具模块ejs/ejs-loader:模板文件编译工具模块css-loader/postcss-loader/style-loader:编译CSS样式代码并放入html内部样式表的工具模块file-loader/url-loader/img-loader/image-webpack-plugin:对图片文件进行压缩及目录位置移动的工具模块与插件sass-loader/node-sass:编译sass文件
结构
dist - 上线文件夹src - 打包配置文件夹webpack.config.js - 打包配置文件package-lock.json - 记录安装包版本与来源package.json - 记录项目所需要模块信息dist > css/fonts/js/scripts - 静态文件文件夹src > components - 组件文件夹src > css - 样式文件夹src > img - 图片文件夹src > js - 逻辑层脚本文件夹src > models - 模型层脚本文件夹src > utils - 工具模块文件夹src > index.html - 视图层HTML文件
组件化
组件化目录结构:对应页面的组件
src > components> cart/detail/haader/index目录> list_item组件文件夹> index.tpl/index.js/index.scss
关于index.js入口文件:
- 如果在组件文件里(src/components/header/list_item/index.js), 就为组件的入口文件,负责引入scss/tpl文件且导出一个函数返回一个带名字和模板的对象
- 如果在项目JS目录下(src/js/index.js),就为逻辑层的主JS文件,负责每个页面的主入口,引入所有组件页面,引入数据模型,且对DOM进行操作,页面渲染
- 如果在模型层目录下(src/models/index.js),每个页面对应的异步数据请求(ajax/axios),组合成html字符串
工具集合
src > utils- config.js:配置API接口的地址- http.js:封装的ajax函数- tool.js:处理事件/正则等
模块

webpack
好处:
- 只加载一个js文件
- 对引入文件的来源有记录
import xx from './xxx' - 会翻译关键字
import代码
缺点:
- 只知道模块相关的事情(仅仅是模块打包工具)
- 刚开始只支持js类型的模块文件
- 后续其他模块类型需要配置
安装
//安装webpacknpm i -D webpack@4.45.0//安装webpack-cli 可以在命令行工具输入webpack相关命令npm i -D webpack-cli@3.3.10
使用
//基础命令webpack//执行npx webpack文件翻译index.jsnpx webpack ./src/js/index.js//打包成功生成 dist/main.js(翻译后的代码)
配置
//配置webpack.config.js//node只支持commonjs写法const path = require('path');module.exports = {entry: path.resolve(__dirname, 'src/js/index.js'),output: {//dist目录默认输出main.js,此时更改名为bundle.jsfilename: 'bundle.js',//出口目录为dist里面的bundle.js文件path: path.resolve(__dirname, 'dist')}}
更名
//一般情况,webpack会自动寻找webpack.config.js配置文件//假如需要更改webpack.config.js => webpack.dev.config.js//1.更改名称为webpack.dev.config.js//2.脚本更改命令 dev => webpack --config webpack.dev.config.js
入口
entry入口不设置名称情况下默认打包后名称为dist/main.js
//entry可以同时配置多个入口 如主入口和子入口module.exports = {...,entry: {main: path.resolve(__dirname, 'src/js/index.js'),sub: path.resolve(__dirname, 'src/js/index.js')},output: {//会根据main/sub.js生成filename: '[name].js',path: path.resolve(__dirname, 'dist')},}//此时,dist目录下生成main.js和sub.js
加载器
属于webpack模块里
//配置模块规则//module对象 => rules数组 => 对象里配置
处理css, jpg, png, scss 需要loader处理相应的模块
图片处理
使用场景:
- 如果图片较大,bundle.js文件里base64图片地址代码过长,造成bundle.js文件体积过大,影响页面加载速度,推荐用file-loader生成jpg文件
- 如果图片较小,因为通过file-loader生成jpg文件时多发送一次HTTP请求,所以推荐url-loader生成base64地址
file-loader
//处理图片//安装file-loadernpm i -D file-loader@6.2.0//配置模块规则//module对象 => rules数组 => 对象里配置module.exports = {...,module: {rules: [{test: /\.jpg|jpeg$/,use: {loader: 'file-loader',//对file-loader进行配置options: {//对生成后的图片名称改为指定的名称//如cat.jpg => cat.jpgname: '[name].[ext]',//将生成后图片文件放入指定的目录//如此一来,打包后图片将存放在dist/imgs/目录里outputPath: 'imgs/'}}}]}}
//尝试引入图片 并打印import catImg from '../cat.jpg';console.log(catImg);//重新打包//浏览器控制台打印f4138709b57185fb12c8f07bbdbc38c1.jpg//并发现dist目录下生成名字为xxxxxxxxxx.jpg的图片//尝试插入图片到页面const img = new Image();img.src = '../dist/' + catImg;app.appendChild(img);//页面显示图片成功
url-loader
//利用url-loader也可以处理图片//与file-loader相似,但是返回DataUrl,即base64格式图片地址//安装url-loadernpm i -D url-loader@4.1.1//配置模块规则//module对象 => rules数组 => 对象里配置module.exports = {...,module: {rules: [{test: /\.jpg|jpeg$/,use: {loader: 'url-loader',//对file-loader进行配置options: {//对生成后的图片名称改为指定的名称//如cat.jpg => cat.jpgname: '[name].[ext]',//将生成后图片文件放入指定的目录//如此一来,打包后图片将存放在dist/imgs/目录里outputPath: 'imgs/'}}}]}}//发现dist目录没有图片文件,但页面正常渲染图片//查看图片元素地址为data:image/jpeg;base64xxxxxx开头的地址
配置:灵活根据图片大小来选择加载器生成图片或生成图片地址
module.exports = {...,module: {rules: [{test: /\.jpg|jpeg$/,use: {loader: 'url-loader',//对file-loader进行配置options: {...,//如图片大于2048字节,单独生成文件//如图片很小,生成base64地址limit: 20480}}}]}}
处理样式
css-loader&style-loader
//安装npm i -D css-loader@5.0.1npm i -D style-loader@2.0.0//配置module.exports = {...,module: {rules: [...,{test: /\.css$/,//loader执行顺序:从后往前/从下往上use: ['style-loader', 'css-loader']}]}}//打包后成功显示样式
sass-loader&node-sass
//安装npm i -D sass-loader@10.1.1npm i -D node-sass@5//配置//将原来匹配css改变为匹配scssmodule.exports = {...,module: {rules: [...,{test: /\.scss$/,use: ['style-loader', 'css-loader', 'sass-loader']}]}}//打包后成功显示样式
处理前缀
postcss-loader&autoprefixer
//低版本浏览器没有css3里面的transform: rotate(45deg);//如何兼容? postcss-loader结合autoprefixer//autoprefixer可以增加厂商前缀 如-webkit-tranform: rotate(45deg)//安装cnpm i -D postcss-loader@4.1.0cnpm i -D autoprefixer@10.2.3//配置module.exports = {...,module: {rules: [...,{test: /\.scss$/,use: ['style-loader','css-loader',//webpack老版本写法 兼容4.x以下{loader: 'css-loader',options: {importLoaders: 2}}'postcss-loader','sass-loader']}]}}//需要在项目根目录新建postcss.config.js且配置module.exports = {plugins: [//引入插件require('autoprefixer')]}//package.json配置增加"browserslist": [//针对市场份额大于1%"> 1%",//最近两个版本"last 2 versions"]//打包后成功显示样式
处理字体
iconfont字体库下载字体
将字体文件放入src/css/fonts
module.exports = {...,module: {rules: [...,{test: /\.(svg|eot|ttf|woff|woff2)$/,use: {loader: 'file-loader',options: {name: '[name].[ext]',outputPath: 'fonts/',}}}]}}//将字体css样式放入index.scss里@font-face {font-family: "iconfont"; /* Project id 2738996 */src: url('../fonts/iconfont.woff2?t=1634310802163') format('woff2'),url('../fonts/iconfont.woff?t=1634310802163') format('woff'),url('../fonts/iconfont.ttf?t=1634310802163') format('truetype');}
//渲染字体const app = document.getElementById('app');app.innerHTML = '<div class="iconfont iconaixin"></div>';//图标字体成功显示
插件
webpack里面的插件
plugins: []
dist目录生成html文件
html-webpack-plugin
//安装npm i -D html-webpack-plugin@4.5.1//配置const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {...,plugins: [new HtmlWebpackPlugin()]}//打包完成时dist目录下自动创建除了引用bundle.js以外的空白的index.html文件//为了解决空白的index.html,可以提供模板scr/index.html//配置module.exports = {...,plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]}//再打包后发现dist目录重新生成指定模板的index.html文件
清理dist目录文件的插件
clean-webpack-plugin
//安装npm i -D clean-webpack-plugin@3.0.0//引入且配置const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {...,plugins: [new CleanWebpackPlugin()]}//打包后发现dist目录先清除后被存入打包文件
sourceMap
帮助于调试代码
默认控制台错误会跟踪到dist/main.js 而不是src/js/index.js
利用sourcemap设置可以实现
//配置项module.exports = {...,//源文件 速度最快devtool: 'eval-source-map',//生成映射关系的map文件 dist/main.js.map 速度稍快//devtool: 'cheap-module-source-map',}
开发服务
webpack-dev-server
注意:使用webpackDevServer是不会生成dist打包文件,但会在内存里生成打包文件,提高效率
提供一个开发服务器实现实时网页更新,不需要频繁的输入打包命令
//安装npm i -D webpack-dev-server@3//配置module.exports = {...,devServer:{//指向dist目录contentBase: './dist',//指定端口prot: 3000}}//修改脚本命令"dev": "webpack-dev-server"
请求转发
使用指定的地址访问指定API地址
//安装axios请求API数据import axios from 'axios';axios.get('http://study.jsplusplus.com/Yixiantong/getHomeDatas').then(res => console.log(res));
一般来说,本地接口跟线上接口很有可能不在同一台服务器上,意味着上面的绝对路径接口地址就不好用了,所以一般会改为相对路径
axios.get('/Yixiantong/getHomeDatas').then(res => console.log(res));//报错:GET http://localhost:8080/Yixiantong/getHomeDatas 404 (Not Found)
但是改为相对路径会报错,所以这样的情况可以做相对接口的请求转发
//当请求http://localhost:8080/Yixiantong时通过webpackDevServer帮助转发给http://study.jsplusplus.com/域名//配置module.exports = {...,devServer:{//指向dist目录contentBase: './dist',open: true,proxy: {//一旦访问匹配到指定开头的路径//'/Yixiantong'//'api/Yixiantong'//以api开头'/api': {//转发到目标的域名地址target: 'http://study.jsplusplus.com/',//对源做了限制changeOrigin: true,//针对请求地址是https时secure: false,//如果有指定的写法 如api/Yixiantong//访问http://study.jsplusplus.com/api/Yixiantong//可以对其路径重写pathRewrite: {//去掉api'^/api': ''}}}}}//此时可以正常访问到API数据
自动刷新
页面刷新
WDS
webpackDevServer 提供一个配置可以定时的去刷新打包打包并刷新页面重新加载静态资源文件
注意:webpackDevServer都是针对开发环境来实现的,生产环境无法使用
//自动刷新控制台提示[WDS]Live Reloading enabled.
//配置module.exports = {...,//监听源代码变化,如有变化重新打包watch: true,watchOption: {ignored: /node_modules/,//不想刷新的太快aggregateTimeout: 300,//每隔一定时间去询问系统,文件是否存在更改poll: 1000}}
热更新
热更新HMR提供实时数据跟新
问题:为什么需要HMR?
如点击或者输入事件时,不能实时更新内容,不会实时渲染
自动刷新和热更新区别:
自动刷新只是重新加载静态文件刷新页面,会导致状态丢失,如输入文本的内容丢失,变量声明的丢失等,热更新则保留状态
//热更新控制台提示[HMR]Waiting for update signal from WDS...
//引入插件//webpack内置的HotModuleReplacementPluginconst webpack = require('webpack');//配置module.exports = {...,devServer:{...,//开启HMRhot: true},plugins: [new webpack.HotModuleReplacementPlugin()]}//控制台显示(说明热更新启动成功)[HMR] Waiting for update signal from WDS...[WDS] Hot Module Replacement enabled.[WDS] Live Reloading enabled.
//webpack-dev-server3.9.0版本 配置写法//引入热更新插件const WebpackCommonConfig = require('./webpack/lib/HotModuleReplacementPlugin');module.exports = {...,entry: {index: path.join(srcPath, 'index.js'),index:['webpack-dev-server/client?http://localhost:8080/','wepack/hot/dev-server',path.join(srcPath, 'index.js')]},devServer:{...,//开启HMRhot: true},plugins: [new webpack.HotModuleReplacementPlugin()]}
如果是样式刷新,热更新是会生效的,且保留状态(style.loader底层有代码实现)
如果是逻辑代码刷新,热更新会失效,不保留状态,需要继续配置
//逻辑代码里//如果开启了 热更新if(module.hot){//参数1: 热更新监测范围//参数2: 热更新后回调自定义的代码内容//只有修改math.js里的代码会触发热更新module.hot.accept(['./math.js'], ()=>{console.log('hello');});}
es6
处理es6代码
babel-loader&@babel/core&@babel/preset-env
//安装npm i -D babel-loader@8.2.2npm i -D @babel/core@7.12.10npm i -D @babel/preset-env@7.12.11//配置module.exports = {...,module: {rules: [...,{test: /\.js$/,loader: 'babel-loader',options: {presets: ['@babel/preset-env']},exclude: /node_modules/}]}}
@babel/polyfill
解决promise和map方法的实现(早期es3没有)
关于:corejs
一个标准的库,提供polyfill es6 es7等方法实现
//安装npm i -D @babel/polyfill@7.12.1//引入import '@babel/polyfill';const promiseArray = [new Promise(()=>{}),new Promise(()=>{})];promiseArray.map(promise => {console.log('promise', promise);});//使用了polyfill后 打包后 体积更大//实现了es6新特性 箭头函数等//按需加载新特性配置module.exports = {...,module: {rules: [...,{test: /\.js$/,loader: 'babel-loader',options: {presets: [['@babel/preset-env'], {useBuiltIns: 'usage',//版本号:3corejs: 3}]},exclude: /node_modules/}]}}
babel/plugin-transform-runtime开发依赖
babel/runtime生存依赖
帮助polyfill改变内库,内部插件 造成的全局污染问题
//安装npm i -D @babel/plugin-transform-runtime@7.15.8npm i -S @babel/runtime@7.15.4//配置module.exports = {...,module: {rules: [...,{test: /\.js$/,loader: 'babel-loader',options: {plugins: [['@babel/plugin-transform-runtime', {"absoluteRuntime": false,"corejs": 3,"helpers": true,"regenerator": true,"useESModules": false,"version": "7.0.0-beta.0"}]]},exclude: /node_modules/}]}}
.babelrc
专属配置babel的文件
主要写两个配置项presets/plugins数组
{"presets": [["@babel/preset-env",{"useBuiltIns": "usage","corejs":3}]],"plugins": [["@babel/plugin-transform-runtime",{"absoluteRuntime": false,"corejs": 3,"helpers": true,"regenerator": true,"useESModules": false,"version": "7.0.0-beta.0"}]]}
Tree Shaking
使用tree shaking概念智能的去除没有使用的代码再打包
注意:只支持ES Module规范,不支持CommonJS规范
//只需要配置生产模式即可触发tree shaking打包不引用的代码mode: 'production'
关于ES Module 和 CommonJS:
- ES Module 是静态引入 编译时引入
import xxx from 'xxx'只能写在首行 不能嵌套在if语句或函数内 - CommonJS 是动态引入 执行时引入
const xxx = require('xxx')
chunks
块
将处理过的css/js/scss文件处理成块 再处理变成静态文件供网页加载
代码分割
splitChunks分割出一个个的chunks,无论同步引入还是异步引入分割
//场景:默认情况会依次打包,所以lodash会被打包2次//a.js(lodash + a业务代码 + 公共代码a)//b.js(lodash + b业务代码 + 公共代码a)//如何实现代码分割?//将lodash提取并单独打包成为一个叫vendor.js文件//将公共代码a提取并单独打包成为一个叫common.js文件//最终,并再需要的时候引入vendor.js或common.js文件//实现代码体积减少optimization: {splitChunks: {chunks: 'all'}}
babel
JavaScript的编译器
转码前(es5 -> es6)需要插件plugins, 预设presets将包含所有的插件
{"presets": [["@babel/preset-env"]],"plugins": []}
关于
babel-polyfill(7.4已废弃)已经包含corejs 和 regenerator,废弃后直接可以用babel直接引用
babel 集成了corejs 和 regenerator,corejs包含了ES6 新语法特性 polyfill(实现),多人维护,比较可靠
缺点:不支持ES6特性的实现(generator 函数)
//关于generator函数function* test(){console.log('settle a');yield 'a';console.log('settle b');yield 'b';}let res = test();console.log(res.next());console.log(res.next());console.log(res.next());
需要另外的库去实现generator函数,叫regenerator, 帮助定义generator函数,如何实现方法
//安装并引入@babel/polyfillimport '@babel/polyfill';//编译代码npx babel ./src/index.js//编译后的代码可以在浏览器内运行了
优化
如何提高webpack性能优化?(开发模式)
优化打包构建速度
- 优化
babel-loader编译范围(越少越好) IgnorePlugin避免不必要的模块noParse避免打包已经打包过的模块happyPack/ParalelUglifyPlugin多进程打包- 自带刷新,热更新
DllPlugin预先打包一次
- 优化
优化打包文件体积
- 图片较少时,采用base64编码格式
- 哈希值
- 懒加载
- 代码分割
忽略插件
IgorePlugin()
plugins: [//忽略moment时间地区插件里除了中国地区以外的时间和地区语言包代码new webpack.IgorePlugin(/.\/locale/, /moment/)]
不打包
noParse
不做打包,但打包生成的代码里有相关代码
noParse: [/vue\.min\.js/],mode: ...
多线程打包
NodeJS 基于JS 单线程
Webpack 基于 NodeJS 单线程 开启多线程打包 提高打包速度
happyPack帮助开启多进程打包
//安装npm i -D happypack@5.0.1//引入和配置const HappyPack = require('happypack');module.exports = {...,module: {rules: [...,{test: /\.js$/,use: ['happypack/loader?id=babel']}]},plugins: [new HappyPack({id: 'babel',loaders: ['babel-loader?cacheDirectory']})]}
代码压缩
UglifyjsWebpackPlugin
压缩js代码
//安装npm i -D webpack-parallel-uglify-plugin@1.1.2//引入和配置const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');module.exports = {...,plugins: [new ParallelUglifyPlugin({uglifyJS: {beautify: false,comments: false}})]}
动态连接库
DLLPlugin
提高打包构建速度
预先打包一次 存放某一个地方 使用的时候引用,类似vue/react,优化打包构建速度
//针对dll的配置//webpack.dll.conf.js//引入webpack自带的dll插件const DllPlugin = require('webpack/lib/DLLPlugin');module.exports = {mode: 'development',entry: {//当项目引入react/react-dom的时候会打包到/dist/react.js文件里react: ['react', 'react-dom']},output: {//打包生成 bundle 的名字 react.dll.jsfilename: '[name].dll.js',//distPathpath: path.join(__dirname, '..', 'dist'),//srctPath//path: path.join(__dirname, '..', 'src'),//定义全局变量 _dll_react 在react.dll.js文件里library: '_dll_[name]'},plugins: [new DllPlugin({name: '_dll_[name]',//mainfest.json 是一个映射的文件 把相关不同的js文件做不同的映射path: path.join(distPath, '[name].mainfest.json')}),new DllReferencePlugin()]}
//脚本"dll": "webpack --config build/webpack.dll.conf.js"
//webpack.dev.conf.js//引入DllReferencePluginconst DllReferencePlugin = require('webpack/lib/DllReferencePlugin');module.exports = {...,plugins: [new DllReferencePlugin({//使用dist/react.mainfest.json文件mainfest: require(path.join(distPath, 'react.mainfest.json'))})]}
处理顺序:
当逻辑代码里引入import React from 'react';/importReactDOM from 'react-dom';会打包成相应的配置和映射文件
减少代码体积的目的:
- 减少代码体积
- 合理分包,不重复加载
- 内存占用更少
哈希值
利用output设置哈希值实现唯一文件内容加载,避免浏览器缓存加载
output: {filename: '[name].[contentHash:8].js',path: distPath}
懒加载
setTimeout(()=>{import('./test.js');}, 3000);
