Webpack4/5学习笔记
为什么要用webpack
1.作用域问题 jquery lodash 自定义对象会绑定window.$ ... 污染window对象
之前是使用 grunt和gulp 任务执行器(利用了js的立即调用函数表达式 IIFE Immediately Invoked Function Expressions)
2.文件分散加载,页面的内容会随着文件的加载而显示
3.打包在一起:可读性,维护性差
构建内库,很少的第三方库,可以用roullup.js
简单命令
1.npm init -y 生成package.json
概念
1.webpack静态模块打包器
2.资源 js css less image 都要交给构建工具(webpack)去处理 怎么处理:
- 告诉webpack起点:入口文件 index.js
- 将依赖关系记录好(比如index.js中 有jquery 和 less),然后将资源文件引进来,形成代码块 chunk,代码块可以将 less编译成css , ES6编译成ES5 ,统一说成是打包
- 打包之后 输出的文件 我们叫做 bundle.js
五个核心概念
1. Entry 入口 指示webpack以哪个文件为入口起点开始打包,分析构建内部依赖图。
2.Output 输出 指示webpack以打包后的资源bundles 输出到哪里去,以及如何命名。
3.Loader 翻译官 让webpack能够去处理那些非javascript文件(webpack自身只理解javascript)。
4.Plugins 插件 开飞机 开航母 可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。
5.mode
- 命令中“-o”的含义就是“输出”的意思,后面紧跟结果文件名。
- 生成环境会压缩代码
- 直接使用node就可以运行 built.js文件
node ./src/built.js
webpack.config.js webpack的配置文件
所有构建工具都是基于node.js平台运行的~模块化默认采用commonjs。
(CommonJS是一种被广泛使用的js模块化规范,核心思想是通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口)
样式文件:能够实现HMR功能,是因为style-loader内部实现了
JS文件:默认不能使用HMR功能=>
解决:需要修改js代码,添加支持HMR功能的代码 (只能热替换入口文件以外的被引入的文件,入口自身不能热替换)
index.js文件代码
在index.js里给需要热模块替换的文件添加代码
import Refresh from '../../../components/RefreshList'
if(module.hot){
module.hot.accept ('./RefreshList.js',function(){
//方法会监听RefreshList.js文件的变化,一旦发生变化,其他模块不会重新打包构建
//会执行里面的回调函数
RefreshList()
})
}
RefreshList文件代码
console.log('我被加载了~');
export default RefreshList
HTML文件:默认不能使用HMR功能,同时导致html文件不能热更新了(不用做HMR功能)=>
解决:修改entry入口,将html文件引入 entry:['./src/js/index.js','./src/index.html']
// resolve用来拼接绝对路径的方法 const {resolve} = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 压缩CSS插件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
// 设置nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'development'
module.exports = {
// 入口起点
entry:'./src/index.js',
// 输出
output:{
filename:'built.js',
// __dirname 是nodejs的变量, 代表的是当前文件的目录绝对路径
path:resolve(__dirname,'build')
// 告诉webpack不要使用箭头
environment: {
arrowFunction: false
}
},
module:{
rules:[ // 详细的loader配置
{
//语法检查: eslint-loader eslint
//设置检查规则: package.json中的eslintConfig中设置~
//airbnb风格: eslint-config-airbnb-base eslint-plugin-import eslint
test: /.js$/,
exclude:/node_modules/,
loader:'eslint-loader',
options:{
//自动修复eslint错误
fix:true
},
{
//js兼容性处理: babel-loader @babel/preset-env @babel/core
//1.只能转换基本语法 @babel/preset-env 如promise 不能转换
//2.按需加载 core-js
test:/\.js$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
//预设:指示babel做怎样的兼容性处理
presets:[
'@babel/preset-env',
{
//按需加载
useBuiltIns:'usage',
//指定core-js版本
corejs:{
version:3
},
// 指定兼容性做到哪个版本
targets:{
chrome:'60',
firefox:'60',
ie:'9',
safari:'10',
edge:'17'
}
}
] //箭头函数会转化为普通函数
}
},
{
test:/\.css$/,
// 执行顺序 : 由后往前
use:[
//创建style标签, 将js中的样式资源插入进行,添加到head中生效
// 'style-loader',
// 取代style-loader : 提取js中的css成单独文件,创建link标签,引入css
MiniCssExtractPlugin.loader,
//将css文件变成commonjs模块加载到js中,里面内容是样式字符串
'css-loader',
// css兼容处理 postcss, 还需在package.json中的browserslist中配置
{
loader:'postcss-loader',
options:{
// 识别postcss语法
ident:'postcss',
plugins:()=>[
//postcss插件
require('postcss-preset-env')()
]
}
}
]
},
{
test:/\.less$/,
use:[
//创建style标签,将样式放入
'style-loader',
//将css文件整合到js文件中
'css-loader',
//将less编译为css
'less-loader'
]
},
{
// 问题: 默认处理不了html中的img图片
// 处理图片资源
test:/\.(jpg|png|gif)$/,
// 下载url-loader file-loader
loader:'url-loader',
options:{
// 图片小于8kb,就会被base64处理
// 优点: 请求数量减少
// 缺点: 图片体积会更大
limit: 8 * 1024,
// 问题: 因为url-loader默认使用es6模块化解析,而html-loader引入图片是commonjs
// 解析时会出问题: [object Module]
// 解决: 关闭url-loader的ES6模块化, 使用commonjs解析
esModule:false
// 给图片进行重命名
// [hash:10]取图片的hash前10位
// [ext]取文件原来的拓展名
name:'[hash:10].[ext]'
}
},
{
test:/\.html$/,
//处理html文件中的img图片(负责引入img,从而能被url-loader进行处理)
loader:'html-loader'
},
] }, plugins:[ // 详细plugins配置
// html-webpack-plugin
// 功能:默认会创建一个空的HTML, 自动引入打包输出的所有资源(js/css)
// 需求:需要有结构的HTML文件
new HtmlWebpackPlugin({
// 复制'./src/index.html' 文件, 并自动引入打包输出的所有资源(JS/CSS) 会自己创建script标签 并引入资源
template:'./src/index.html',
//压缩html代码
minify:{
//移除空格
collapseWhitespace:true,
//移除注释
removeComments:true
}
}),
new MiniCssExtractPlugin({
//配置提取后的css文件路径
filename:'css/built.css'
}),
// 压缩css
new OptimizeCssAssetsWebpackPlugin()
], // 模式 mode:'development',
//压缩js代码 // mode:'production'
// 开发服务器 基于node.js搭建,内部使用了express框架 devServer: 用来自动化(自动编译、自动打开浏览器、自动刷新浏览器)
// 特点:只会在内存中编译打包,不会有任何输出。
// 启动devServer指令为: npx webpack-dev-server (因为是本地的包,所以要npx启动)
devServer:{
contentBase:resolve(__dirname,'build'),
// 启动gzip压缩
compress:true,
//端口号
port:3000,
open:true,
//开启HMR功能
hot:true,
//页面实时刷新
inline:true
},z
//配置路径的规则,可以配置引入资源的默认路径
resolve: {
// 配置别名
alias: {
style: path.resolve(__dirname,'src/style')
}
// 配置省略文件路径后缀名
extensions:['.ts','.js']
}
}
package.json
"browserlist":{ "development":[ // 开发环境 兼容最近的浏览器,如果要使用开发环境 要设置node环境变量: process.env.NODE_ENV = 'development' "last 1 chrome version",
"last 1 firefox version",
"last 1 safari version",
], "production":[ // 生产环境:webpack默认生产环境
">0.2%", //大于99.8%的浏览器
"not dead", //不要兼容已经死了的浏览器 比如ie10
"not op_mini all"
] },
"eslintConfig":{
"extends":"airbnb-base" //配置eslint
}
进阶
减小JS文件大小:mini-css-extract-plugin 提取CSS文件
css兼容性处理: post--> postcss-loader postcss-preset-env (postcss 要在webpack中运行,需要这两个插件)
开发环境调试代码:source-map
当我们执行打包命令后,我们发现bundle的最后一行总是会多出一个注释,指向打包出的bundle.map.js(sourcemap文件)。 sourcemap文件用来描述源码文件和 bundle 文件的代码位置映射关系。基于它,我们将bundle文件的错误信息映射到源码文件上。
除开’source-map’外,还可以基于我们的需求设置其他值,webpack——devtool一共提供了7种SourceMap模式:
eval:每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL.
source-map:生成一个SourceMap文件
hidden-source-map:和 source-map一样,但不会在 bundle 末尾追加注释
inline-source-map:生成一个 DataUrl 形式的 SourceMap 文件.
eval-source-map:每个module会通过eval()来执行,并且生成一个DataUrl形式的 SourceMap
cheap-source-map:生成一个没有列信息(column-mappings)的SourceMaps文 件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)
cheap-module-source-map:生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。
要注意的是,生产环境我们一般不会开启sourcemap功能,主要有两点原因:
1.通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产物有soucemap文件的话,就意味着有暴漏源码的风险。
2.我们可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追求不同(生产环境追求更小更轻量的bundle)。
技巧
复用loader
const commonCssLoader = [
MiniCssExtractPlugin.loader, //将css文件变成commonjs模块加载到js中,里面内容是样式字符串 'css-loader', // css兼容处理 postcss, 还需在package.json中的browserslist中配置 { loader:'postcss-loader', options:{ // 识别postcss语法 ident:'postcss', plugins:()=>[ //postcss插件 require('postcss-preset-env')() ] } } ]
rules:[
{
test:/.\css$/,
use:[...commonCssLoader]
},
{
test:/\.less$/,
use:[...commonCssLoader,'less-loader']
}
]
优化
开发环境
- 优化打包构建速度
- 优化代码调试
问题:修改了CSS ,整个文件都重新打包了
解决:HMR(hot module replacement) 热模块替换 / 模块热替换
作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块) 能够极大的提升构建速度
生产环境
- 优化打包构建速度
- 优化代码运行的性能
常见的代码分离方法:
1.入口起点:使用entry配置手动地分离代码
// entry: resolve(__dirname, './src/index.js'),
entry: {
index: resolve(__dirname, './src/index.js'),
another: resolve(__dirname, './src/another.js'),
},
Entry 进行代码分离的话,每个引入lodash的文件都会把lodash打包一次,如果5个文件引入,那么打包后的体积就会大 553KB * 5
定义公共文件
entry: {
index: {
import: resolve(__dirname, './src/index.js'),
dependOn: 'shared'
},
another: {
import: resolve(__dirname, './src/another.js'),
dependOn: 'shared'
},
shared: 'lodash'
}
2.防止重复:使用Entry dependencies 或者 SplitChunksPlugin 去重和分离代码
optimization: {
// 抽离公共文件
splitChunks: {
chunks: 'all'
}
},
3.动态导入:动态导入,通过模块的内联函数调用来分离代码
example1. async-module.js
async function getComponent() {
// 返回一个Promise
return import('lodash').
then(({ default: _ }) => {
const element = document.createElement('div');
element.innerHTML = _.join(['A', 'B', 'C'], '<=>')
return element;
})
}
getComponent().then((element) => {
document.body.appendChild(element);
})
import './async-module';
example2:
const button = document.createElement('button');
// webpackChunkName 魔法注释,修改打包后的文件名
button.textContent = 'add';
button.addEventListener('click', () => {
import(/* webpackChunkName: 'changeName' webpackPrefetch: true */'./math').then(({ add }) => {
button.textContent = add(10, 10);
})
})
缓存第三方库
optimization: {
splitChunks: {
// 缓存第三方库: 将所有的第三方库打包到一个文件中,在浏览器中缓存,由于这个库不频繁更新,可以提升我们的首屏打开速度和节省我们的流量
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'cacheLibraries',
chunks: 'all'
}
}
}
}
一、常用操作/指令
mkdir 创建一个子目录
mv (move的缩写) 移动
// babel-loader 在webpack里应用babel解析ES6的桥梁
// @babel/core babel核心模块
// @babel/preset-env babel预设,一组babel插件的集合
// @babel/runtime babel运行时需要
// @babel/plugin-transform-runtime 在需要regeneratorRuntime的地方,自动require导包
安装babel
yarn add babel-loader @babel/core @babel/preset-env -D
安装eslint
yarn add eslint eslint-loader -D
配置eslintrc.json ?
在这个网站 https://eslint.org/demo 直接下载
二、webpack.config.js 核心概念
chunk :多个文件组成的一个代码块,例如把一个可执行模块和它所有依赖的模块组合成一个 chunk 这体现了webpack的打包机制。
loader :文件转换器,例如把es6转换为es5,scss转换为css。
plugin :插件,用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
module.exports = { entry: { bundle1:'./main1.js',
bundle2:'./main2.js'
}, output:{ filename:'[name].js' } }
三、Code Splitting
build.js
文件中(文件名取决与在webpack.config.js
文件中output.filename
), 但是在大型项目中, build.js
可能过大, 导致页面加载时间过长. 这个时候就需要code splitting
, code splitting
就是将文件分割成块(chunk)
, 我们可以定义一些分割点(split point)
, 根据这些分割点对文件进行分块, 并实现按需加载.
遇到的问题:
首先是正常启动服务,编译成功,但是打包后apk运行失败
然后重新拉取代码,安装包后报下面的错
1.编译报错
查找报错原因:
1. 查看依赖版本和原来正常运行的APP之间版本区别
2.删除掉index.js和APP.js中的代码 ,随便写个console.log, 看看是不是react-redux的问题
此方法替换后,还是报错。
于是把成功运行的APP的node_modules包复制过来,成功运行和打包
总结: 重新拉取代码,启动程序,程序可以启动的话,才能去编译。
不要因为前面可以启动程序,就想当然的以为重新拉取的项目也可以启动。
后来发现报这个错误
老项目可能修改过node_modules里面的文件,所以重新安装的就没用
-
使用.env.development/.env.production设置环境变量
目录结构
原理:
1,利用node的fs模块读取文件处理成对象
2,用webpack.DefinePlugin插件,设置process.env
create-react-app eject 后 有个env.js文件,用来读取环境变量文件并转化为对象
/* 像vue-cli3 新版create-react-app 一样规定环境变量的Key必须以(VUE_APP_) (REACT_APP_) 开头 */
.env.development
# 是否启动浏览器 BROWSER=none # 缓存的命名空间 REACT_APP_STORE_NAMESPACE=HNAJYJZH # 引入依赖脚本 REACT_APP_INCLUDES_JS= # 初始化脚本 REACT_APP_MAIN_JS=<script src="./main/web.js"></script>
.env.production
GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false PUBLIC_URL=./ # 缓存的命名空间 REACT_APP_STORE_NAMESPACE=HNAJYJZH # 引入依赖脚本 REACT_APP_INCLUDES_JS=<script src="cordova.js"></script> REACT_APP_MAIN_JS=<script src="./main/mobile.js"></script>
public文件夹下的index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="应用首页" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>APP首页</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> %REACT_APP_INCLUDES_JS% </body> %REACT_APP_MAIN_JS% </html>
压缩前的web.js文件
import axios from 'axios'; import qs from 'qs'; import toLower from 'lodash/toLower'; import get from 'lodash/get'; const API_BASE_URL = '/hnajyjzh-ide/'; /** * WEB端底层API桥接封装 * @date 2021-4-11 01:15:53 * @author Leon<leonooo@163.com> */ // 服务器端API接口 // 后台接口底层实现 function request(params) { const method = toLower(get(params, 'method', 'get')); axios({ url: API_BASE_URL + params.url, method, timeout: 1000 * 10, headers: params.headers, data: method === 'post' ? qs.stringify(params.data) : params.data, }).then((data) => { params.success(data); }).catch((err) => { params.error(err); }); } function onload() { window.emitter.on('request', request); window.emitter.emit('render', { baseUrl: API_BASE_URL, vconsole: false, }); } window.addEventListener('load', onload, false);
abc.js文件 复制压缩后的文件到变量中的文件夹下
const path = require('path'); module.exports = { mode: process.env.NODE_ENV || 'production', entry: './bridges/web.js', output: { filename: 'web.js', path: path.resolve(__dirname, '../public/main'), }, module: { rules: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: ['@babel/preset-env'] }, }], }, };
copy.js
const path = require('path'); const fs = require('fs-extra'); const www = path.join(__dirname, '../../app-cordova/www'); const build = path.join(__dirname, '../build'); const public2 = path.join(__dirname, '../public'); fs.removeSync(www); // 创建目录 fs.ensureDirSync(path.join(build, 'main')); // 拷贝文件 拷贝a文件 给 b文件 fs.copyFileSync(path.join(public2, 'main/mobile.js'), path.join(build, 'main/mobile.js')); fs.copyFileSync(path.join(public2, 'main/web.js'), path.join(build, 'main/web.js')); // 复制文件或目录 该目录可以包含内容 fs.copySync(build, www); // 删除文件或目录 该目录可以包含内容 如果该路径不存在,则静默不执行任何操作。 fs.removeSync(path.join(www, 'main/web.js')); fs.removeSync(path.join(www, 'robots.txt'));
package.json 命令
"scripts": { "build": "react-scripts build", "www": "run-s bridge:* copy", "copy": "node ./scripts/copy.js", "format": "eslint ./src --fix", "update": "ncu", "bridge:web": "webpack --config scripts/abc.js", "bridge:app": "webpack --config ./scripts/webpack.config.mobile.js", "prepare": "cd .. && husky install ui/.husky" },
进行大量的运算,又不阻塞主线程: 首先考虑的是异步,但是也可能导致浏览器假死状态
允许我们将耗时的,单纯的js处理逻辑放在后台处理
升级笔记
autoprefixer
是个前端都能懂,就是css自动加前缀,配合postcss使用,如果想更加详细的postcss配置,请移步移动端适配。
html-webpack-plugin
这个插件就是为你生成一个已经自动注入打包后的js的html文件
case-sensitive-paths-webpack-plugin
这个插件就是防止不同的系统下对于大小写的问题导致路径出错
InterpolateHtmlPlugin
这个插件是配合html-webpack-plugin一起使用的,允许你在index.html中使用变量
// 我们在create-react-app生成的项目中public下的index.html可以看到下面的代码 <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
这里的PUBLIC_URL就是一个变量,当webpack打包的时候将你设置的值替换它。
WatchMissingNodeModulesPlugin
这个也是方便开发的插件,当你在一个项目里引用了没有安装的node_modules,webpack会报错module not found,然后你安装完成后,需要自己手动重新启动服务。WatchMissingNodeModulesPlugin这个插件就是帮你省去这步骤,你安装玩缺失的module,webpack会自动重启。
eslintFormatter
emmmm....eslint
ModuleScopePlugin
这个插件是防止你在当前项目中引入非本项目的资源(即src和node_modules文件夹之外的),因为只有src文件夹下的代码才会被babel转译。
webpack.NamedModulesPlugin
这个插件的作用是在热加载时直接返回更新文件名,而不是文件的id,方便开发人员定位。具体
webpack.DefinePlugin
DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用。比如我们区分测试环境和正式环境的域名
new webpack.DefinePlugin({ PRODUCTION: process.env.NODE_ENV === "production", DEVELOPMENT: process.env.NODE_ENV === "development" })
复制代码
然后在js代码中,如果代码中使用了eslint,还需要在package.json中将这个变量声明一下
"eslintConfig": { "extends": "react-app", // 继承已启用的规则配置 "globals": { "PRODUCTION": false // false不允许PRODUCTION变量重写,只读属性 "DEVELOPMENT": true // true允许DEVELOPMENT变量重写 } }
if(PRODUCTION){ domain = www.production.com }else{ domain = www.development.com }
webpack.HotModuleReplacementPlugin
模块热替换插件,cereate-react-app中只有css变换才有热替换,js变更会刷新页面。
IgnorePlugin
webpack打包时忽略打包的资源。
webpack.config.prod.js
extract-text-webpack-plugin => mini-css-extract-plugin
=> 
将CSS从JS bundle中拆分出来,减小JS的文件大小,加载速度更快, 并合并成一个css文件,并改为link的方式引入
webpack-manifest-plugin
这个就是在build打包的过程中生成一个JSON文件,用来展示编译之前得文件和编译以后的文件的映射关系。
sw-precache-webpack-plugin
SWPrecacheWebpackPlugin是一个webpack插件,用于使用service worker来缓存外部项目依赖项。
其他知识:
/*#__PURE__*/
webpack压缩(tree-shaking树摇)的时候,如果看到/*#__PURE__*/这个标志,说明他是纯函数,如果没有调用它,会直接把它删除了,减少代码体积
optionalDependencies (可选依赖)
如果一个依赖关系可以被使用,但你希望npm在找不到它或安装失败的情况下继续进行,那么你可以把它放在optionalDependencies对象中。例如你使用mac os系统 并在项目中安装了依赖a,但是你团队中有其他成员使用Linux系统进行开发, 然而在Linux系统中并不存在a这个包, 这种情况下就可以使用optionalDependencies
除此之外, 如果有一些依赖包即使安装失败,项目仍然能够运行或者希望npm继续运行,就可以使用optionalDependencies。另外optionalDependencies会覆盖dependencies中的同名依赖包,所以不要在两个地方都写。