前言
如果你还不会webpack,就来一起学习一下吧,本文主要是依据webpack5 来进行学习
基础篇
mode
告诉webpack使用相应模式进行内置优化
- development
- 自动会设置 devtool: 'eval'
- production (默认)
devtool
控制是否生成source map 以及如何生成source map
- eval (默认)
- false
- source-map
- eval-source-map base64的方式放在eval 后面
- Inline-source-map 在最下面有一个sourceMappingUrl = base64
- cheap-source-map。只有行信息,没有列信息
- cheap-module-source-map(推荐) 对loader 处理的文件展示更加友好
- hidden-source-map // 有map 文件生成,但是定位不到源代码,需要手动加载到环境下
- nosource-source-map // 会生成map 文件,但是没有源文件提示
设置规则
(inline | hidden | eval )(nosources)(cheap|cheap-module )sourcemap
source Map
可以根据转换后的代码再次转换为源代码,方便定位调试
当将devtool 设置为source-map 的时候,在打包后,就会额外生成一个main.js.map 文件,格式化后,我们发现里面大致有这几个字段
version: 3 // 版本
sources: [] // 告诉我们将来的map文件是通过哪个源文件转换来的
names: [] // 对names里面的字符进行特殊的处理
mappings: 'xxx' // 类似于映射算法
file: 'x x x' // 源文件名
sourcesContent: [] // 源文件备份
sourceRoot: '' // 记录sourceMap文件的根路径
entry
打包的目录起点
entry: "./src/index.js",
output
-
path : 打包资源输出到哪一个目录
-
filename: 打包输出的文件名
-
publicPath: "", index.html内部引用打包后的js路径,默认是"",
拼接的规则就是拿到前面的域名 + publicPath + filename
output: {
path: path.resolve(__dirname, "dist"), // 打包的资源输出到哪个目录
filename: "main[hash:6].js",
publicPath: "",
assetModuleFilename: "[name].[hash:6].[ext]", // 打包统一的目录下的名称
},
resolve
配置当前模块解析规则
resolve: {
// 配置模块解析规则
extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
alias: { // 配置别名
"@": path.resolve(__dirname, "src"),
},
},
devServer
使用webpack-dev-server 的一系列配置
- publicPath: 指定本地服务所在的目录,默认值为 / (项目所在目录)
- contentBase: 打包之后的资源如果依赖了其他没有打包的资源,则告知去哪里找,绝对路径
- watchContentBase: 和contenBase 配套,进行监听热更新
devServer: {
// webpack-dev-server 配置
// 配置额外的静态文件目录
contentBase: path.resolve(__dirname, "dist"),
watchContentBase: true,
compress: true, // 默认为false, 开启服务端gzip 的压缩
port: 8080,
open: true,
hot: 'only';, // 热更新
// hotOnly: true, // 后面文档更新为了hot 报错信息不会刷新页面
publicPath: "/", // 后面文档更新为了static
// historyApiFallback: true
proxy: {}
},
建议output 里面的publicPath 和devServer 里面的publicPath 设置为一样的
proxy
设置代理
proxy: {
// http:localhost:8000/api/users
// https://api.github.com.api/api/users
"/api": {
targey: "https://api.github.com", // 实际代理的地址
pathRewrite: {
"^/api": "", // 将地址重写,将/api重写为“”, 想当于https://api.github.com.api/users
},
changeOrigin: true, // 修改host
},
},
loader
loader 是一个模块,将一些文件转换为webpack可以识别的模块,例如css模块不能进行webpack 进行打包,需要进行转换
css-loader 将css文件转换为一个对象,让webpack 能够识别css 语法,style-loader 将转换后的对象应用到页面上展示出style来,利用less-loader 来进行处理less文件(less 会把less 语法转为css)
-
css-loader
-
style-loader
-
less-loader
配置loader 语法
在webpack.config.js
module: {
rules: [ // 添加多个不同文件的loader
{
test: /\.css$/,
use: [ // 从下往上,从右边往左执行
{
loader: "style-loader",
// options: "" 暂时不需要参数,所以不传
},
{
loader: 'css-loader'
}
]
},
{ test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"] }, // 简写方式
]
}
module: {
// loader 分类 pre前置 normal inline post
rules: [
{ test: /\.txt$/, type: "asset/source" },
// { test: /\.css$/, use: ["style-loader", "css-loader"] },
{
test: /\.css$/,
use: [
{
loader: "style-loader",
// options: "",
},
{
loader: "css-loader",
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
// require("autoprefixer"),
require("postcss-preset-env"),
],
},
},
},
],
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
// require("autoprefixer"),
require("postcss-preset-env"),
],
},
},
},
"less-loader",
],
},
],
},
我们发现在less 和css 中都会用到一样的loader,有一些冗余了,所以可以单独的拿出来一个文件进行配置
项目根目录下创建一个postcss.config.js
module.exports = {
plugins: [require("postcss-preset-env")],
};
然后model 就可以直接写postcss-loader,就会自动找到他的配置文件postcss.config.js
{
test: /\.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1, // 往前找一个,css-loader需要往前找一个loader执行,再次调用postcss-loader
},
},
"postcss-loader",
"less-loader",
]
}
打包图片 file-loader
将图片也当作模块导入,webpack 默认不能处理,需要用到file-loader
// 引入图片
import oImagesrc from "./img/es.png";
import "./css/img.css";
function packImg() {
// img 标签,src 属性
const oEle = document.createElement("div");
// 创建img标签,设置src
const oImg = document.createElement("img");
const requireEsModul = require("./img/es.png");
console.log(requireEsModul, "---->requireEsModul");
// file-loader 处理后,会将require 导入后的变为一个对象,里面的defalut属性才是图片地址
// oImg.src = require("./img/es.png").default;
// 如果file-loader 设置了options属性 esModule: false 则可以直接使用
// oImg.src = require("./img/es.png");
// 利用import 将图片引入来进行使用
oImg.src = oImagesrc;
oEle.appendChild(oImg);
// 设置背景图片
const oBgImg = document.createElement("div");
oBgImg.className = "bgBox";
oEle.appendChild(oBgImg);
return oEle;
}
document.body.appendChild(packImg());
- 使用require 导入图片,如果不给file-loader 配置esmodule: false, 则需要.default 使用
- 添加esmodule: false
- 采用import xxx from 图片资源地址
webpack.config.js 配置
{
test: /\.(png|svg|gif|jpe?g)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false, // 是否转化后的包裹成esModule
// 修改打包的图片的名称
/**
* 占位符号
* [ext]: 扩展名称
* [name]:文件名称
* [hash]: 文件内容产出的hash
* [contentHash]: hash值
* [hash:<length>] 截取hash 长度
* [path]: 路径
*/
name: "[name].[hash:6].[ext]",
outputPath: "img",
// name: "img/[name].[hash:6].[ext]", 简写设置文件打包后的地址以及名称等
},
},
],
},
打包图片的时候还有一个是url-loader,使用和file-loader差不多,但是
url-loader 会将图片以base64来进行打包到代码中,减少请求次数,而file-loader则会将图片拷贝到dist 目录下,分开请求,在url-loader 内部也可以调用file-loader. 利用limit 属性
{
test: /\.(png|svg|gif|jpe?g)$/,
use: [
{
loader: "url-loader",
options: {
esModule: false,
name: "img/[name].[hash:6].[ext]",
limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
},
},
],
}
webpack5不需要再去配置file-loader 或者url-loader,他新增了一个asset module type 模块
- asset/resource == file-loader
- asset/inline == url-loader
- asset/source == raw-loader
- asset. url-loader + limit
// 01 方式一
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset/resource",
// 打包到统一的目录下面 这里也可以在output 下面配置。与这里的区别是,output配置是只要经过asset 都打包到统一的目录下面
generator: {
filename: "img/[name].[hash:6][ext]",
},
},
// 02 方式二
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]",
},
parser: {
dataUrlCondition: { // 相当于limit
maxSize: 25 * 1024,
},
},
}
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
// 只要经过asset 都打包到统一的目录下面
assetModuleFilename: "[name].[hash:6][ext]",
},
asset 处理图标字体资源
browerslistrc 配置
用来配置项目中兼容哪些平台 可以在caniuse.com/中查看(https:/…
安装webpack的时候,已经默认安装配置了,在nodeModules 中的 browserslist这个包,可以利用npx browserslist 来查看当前兼容的浏览器版本
两种配置方法:
- package.json
"browserlist": [
">1%",
"last 2 version",
"not dead"
]
- 项目下新建 .browserslistrc
>1%
last 2 version
not dead
postcss
就是通过javascript 来转换样式的工具
比如说给css 补充前缀,做一些css的样式做一些兼容性的处理满足更多的浏览器
npm i postcss posts-cli
- postcss。相当于一个转换样式的工具
- postcss-cli. // 安装后,可以在命令行直接使用npx postcss
npx postcss -o ret.css ./src/test.css // 将test.css 输出到ret.css 中
安装autoprefixer 插件 用来自动安装前缀( autoprefixer.github.io/ 添加前缀)
npx postcss --use autoprefixer -o ret.css ./src/test.css // 将test.css 输出到ret.css 中,添加前缀
如果很多样式都需要添加前缀,所以我们利用postcss-loader 这个loader,在配置文件中设置 npm i postcss-loader
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [require("autoprefixer")], // 利用插件
},
},
},
]
},
]
}
postcss-preset-env 预设(插件的集合)
集合了很多的常见的插件的集合
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
// require("autoprefixer"),
require("postcss-preset-env"),
],
},
},
babel-loader配置
babel 简单了解
安装babel核心包
npm i @babel/core -D
安装脚手架执行babel
@babel/cli -D
安装工具包
@babel/plugin-transform-arrow-function
@babel/plugin-transform-block-scoping
执行使用babel
npx babel src/babeltest.js --out-dir build --plugins=@babel/plugin-transform-arrow-functions
- npm i babel-loader 安装babel-loader
- npm i @babel/preset-env 安装预设 或者安装自己需要的plugin
{
test: /\.js/,
exclude: /node_modules/, // node_modules中的不做处理
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"], // 预设,就可以不用一个个写plugin
// 也可以在后面直接配置指定浏览器、
// presets: [["@babel/preset-env",{ targets: "chrome 91" }]],
plugins: [],
},
},
],
},
babel-loader 在webpack 打包的时候,是会根据.browserslistrc 文件来进行转换的
所以可以把babel-loader 配置文件单独拿出去
- bable.config.js(json cis mjs)
- babelrc.json(js)
项目根目录下新建babel.config.js 文件
module.exports = {
presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
};
然后webpack.config.js 就可以改为
{
test: /\.js$/,
use: ['babel-loader']
}
polyfill
Babel 的预设不一定可以将所有的语法都能转为浏览器兼容可以使用的,
所以当遇到最新的语法的时候,需要使用polyfill
Webpack5 之前需要安装 @babel/polyfill npm i @babel/polyfill --save, 但是打包会消耗更多时间,因为不能按需配置
所以webpack5只要 core-js. 和 regenerator-runtime,不需要@babel/polyfill
npm i core-js regenerator-runtime
然后给babel.config.js 中进行配置
useBuiltIns
- usage
- entry
module.exports = {
presets: [
[
"@babel/preset-env",
{
// useBuiltIns: false, // 不对当前js处理做polyfill 填充
useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会默认使用2版本,所以需要指定版本corejs 为3
// useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果需要按需加载,需要在源代码中添加引入 core-js 和 regenerator-runtime
// import "core-js/stable"
// import "regenerator-runtime/runtime"
corejs: 3,
},
],
],
};
ts-loader
{
test: /\.ts$/,
use: ["ts-loader"],
}
转换ts 文件 ,将ts 文件语法转为javascript, 但是如果文件中用到新特性之类的,还是需要babel-loader 来进行添加polyfill转换,安装**@babel/preset-typescript**
{
test: /\.ts$/,
use: ["babel-loader"],
},
module.exports = {
presets: [
[
"@babel/preset-env",
{
// useBuiltIns: false, // 不对当前js处理做polyfill 填充
useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会使用2版本,所以需要指定版本corejs 为3
// useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果
// 需要按需加载,需要在源代码中添加
corejs: 3,
},
],
["@babel/preset-typescript"],
],
};
但是使用babel-loader 如果代码有语法错误,在编译阶段,是不会暴露出来的,只会在运行的时候发现,但是使用ts-loader 却可以
这个时候,我们即希望在编译的时候进行语法校验,也希望babel-loader进行polyfill 填充,所以利用pack.json 方式,在build的时候,同时利用tsc检查一个语法
"tscck": "tsc --noEmit" // 只会校验语法,但是不会产出新的js 文件
"scripts": {
"start": "webpack serve",
"build": "npm run tscck && webpack",
"tscck": "tsc --noEmit"
},
plugin
loader 和plugin 区别
loader 对特定的模块类型进行转换, 读取某一个资源类型的内容时候使用
plugin 可以做更多的事情,可以在webpack 打包的任意生命周期中做一些事情
clean-webpack-plugin
清空dist 目录
html-webpack-plugin
配置打包后的index.html
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWbpackPlugin = require("html-webpack-plugin");
webpack.config.js
plugins: [
new HtmlWbpackPlugin({ template: "./src/index.html" }),
new CleanWebpackPlugin(),
],
definePlugin
webpack 自带的默认的插件,可以使用全局的常量
const { DefinePlugin } = require("webpack");
// 比如配置一个BASE_URL的常量
plugins: [
new DefinePlugin({
BASE_URL: 'lalala', // 注意: 这里会把这个值直接赋值到常量中去
}),
],
copy-webpack-plugin
有时候,public 中不希望webpack进行打包,所以需要进行拷贝
注意: 这里需要9版本,10版本会报错
const CopyWebpackPlugin = require("copy-webpack-plugin");
new CopyWebpackPlugin({
patterns: [
{ from: "public" }, // 不写to 的话,会自动复制到配置的output 这个目录
// { from: "other", to: "public" },
],
webpack-dev-server
可以用webpack 打包的时候监听,用vscode 的live server 来进行查看,但是不能局部刷新,也可以在配置文件里面配置watch: true, 同样,有性能问题,
webpack-dev-server 可以实现局部刷新打包
npm i webpack-dev-server
"start": "webpack serve",
"build": "webpack"
"wacth": "webpack --watch"
实现plugin
webpack.config.js
为了区分生产环境打包配置和开发环境打包配置,我们进行了对webppack.config.js 进行拆分配置。分为三个文件,一个基础文件配置,在生产和开发都会用到,然后一个生产环境配置,一个开发环境配置。通过利用 webpack-merge 将配置文件合并
首先需要修改我们的package.json 文件中的script, 将打包命令执行的webpack配置文件目录通过--config指定为我们想要执行的那个文件,然后通过 --env 来传入当前环境的参数
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production",
"serve": "webpack serve --config ./config/webpack.common.js --env development",
},
在根目录下新建config 文件夹,里面创建四个文件
- webpack.common.js
- webpack.dev.js
- webpack.pro.js
- pathUtils.js // 来配置绑定当前上下文路径
因为我们在写配置别名和output的时候,需要传入一个绝对路径,但是目前我们修改完后,webpack的配置文件路径也就对应不上了,所以利用pathUtils.js 里面的方法来保存一个绝对路径地址。
// pathUtils.js
const path = require("path");
// 获取当前运行地址
const currentDir = process.cwd();
// 根据当前根目录生成绝对路径地址
const resolveCurrent = (relativePath) => {
return path.resolve(currentDir, relativePath);
};
module.exports = resolveCurrent;
然后来配置我们的webpack.common.js 文件,我们把公共的配置都抽离到这个文件中,然后利用 webpack-merge 将配置文件合并
const { merge } = require("webpack-merge");
// 导入确定路径的utils函数
const resolveCurrent = require("./pathalis");
// 导入公共插件
const HtmlWbpackPlugin = require("html-webpack-plugin");
// 导入其他配置文件
// 导入生产环境webpack配置
const prodConfig = require("./webpack.prod");
// 导入开发环境webpack配置
const devConfig = require("./webpack.dev");
// 导出webpack配置文件
// 这里导出一个函数是为了接受package.json里面传入的环境参数
module.exports = (env) => {
// 根据传入环境变量判断
const isProduction = env.production;
// 根据当前打包模式判断要和哪一个配置合并
const config = isProduction ? prodConfig : devConfig;
// 利用webpack-merge合并配置文件并返回
const mergeConfig = merge(baseConfig, config);
return mergeConfig;
};
// // 定义对象,保存基础配置信息
const baseConfig = {
entry: "./src/index.js",
resolve: {
// 配置模块解析规则
extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
alias: {
"@": resolveCurrent("./src"), // 修改路径
},
},
output: {
path: resolveCurrent("./dist"), // 打包的资源输出到哪个目录
filename: "main[hash:6].js",
publicPath: "",
},
module: {
// loader 分类 pre前置 normal inline post
rules: [
// { test: /\.css$/, use: ["style-loader", "css-loader"] },
{
test: /\.css$/,
use: [
{
loader: "style-loader",
// options: "",
},
{
loader: "css-loader",
options: {
importLoaders: 1,
},
},
{
loader: "postcss-loader",
// options: {
// postcssOptions: {
// plugins: [
// // require("autoprefixer"),
// require("postcss-preset-env"),
// ],
// },
// },
},
],
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"postcss-loader",
// {
// loader: "postcss-loader",
// options: {
// postcssOptions: {
// plugins: [
// // require("autoprefixer"),
// require("postcss-preset-env"),
// ],
// },
// },
// },
"less-loader",
],
},
// {
// test: /\.(png|svg|gif|jpe?g)$/,
// use: [
// {
// loader: "url-loader",
// options: {
// esModule: false, // 是否转化后的包裹成esModule
// // 修改打包的图片的名称
// // 占位符号
// // [ext]: 扩展名称
// // [name]:文件名称
// // [hash]: 文件内容产出的hash
// // [contentHash]: hash值
// // [hash:<length>] 截取hash 长度
// // [path]: 路径
// name: "[name].[hash:6].[ext]",
// // name: "img/[name].[hash:6].[ext]",
// outputPath: "img",
// limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
// },
// },
// ],
// },
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset",
// 只需要经过asset 都打包到统一的目录下面
generator: {
filename: "img/[name].[hash:6][ext]",
},
parser: {
dataUrlCondition: {
maxSize: 25 * 1024,
},
},
},
{
test: /\.js/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
// options: {
// presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
// plugins: [],
// },
},
],
},
{
test: /\.ts$/,
use: ["babel-loader"],
},
],
},
plugins: [
new HtmlWbpackPlugin({
template: "./src/index.html",
title: "lalala",
}),
],
};
公共的配置完成后,只需要写入不同环境下的配置就可以了
在webpack.prod.js 中写入生产环境配置
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
mode: "production",
devtool: "source-map",
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{ from: "public" },
// { from: "other", to: "public" },
],
}),
],
};
同理,在webpack.dev.js中写入开发环境配置
console.log(1111);
const path = require("path");
const HtmlWbpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { DefinePlugin } = require("webpack");
module.exports = (env) => {
console.log(env, "---->dfadsfs");
const isProduction = env.production;
return {
// watch: true, // 默认为false
mode: "development",
entry: "./src/index.js",
resolve: {
// 配置模块解析规则
extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
alias: {
"@": path.resolve(__dirname, "src"),
},
},
devtool: "source-map",
output: {
path: path.resolve(__dirname, "dist"), // 打包的资源输出到哪个目录
filename: "main[hash:6].js",
publicPath: "",
assetModuleFilename: "[name].[hash:6].[ext]", // 打包统一的目录下的名称
},
target: "web",
devServer: {
// webpack-dev-server 配置
// 配置额外的静态文件目录
contentBase: path.resolve(__dirname, "dist"),
watchContentBase: true,
compress: true, // 默认为false, 开启服务端gzip 的压缩
port: 8080,
open: true,
hot: true, // 热更新
hotOnly: true, // 报错信息不会刷新页面
publicPath: "/",
// historyApiFallback: true
proxy: {
// http:localhost:8000/api/users
// https://api.github.com.api/api/users
"/api": {
targey: "https://api.github.com", // 实际代理的地址
pathRewrite: {
"^/api": "", // 将地址重写,将/api重写为“”, 想当于https://api.github.com.api/users
},
changeOrigin: true, // 修改host
},
},
},
/**
* loader 是什么,为啥用loader
* loader 是一个模块,将一些文件转换为webpack可以识别的模块,例如css 需要进行转换
*/
module: {
// loader 分类 pre前置 normal inline post
rules: [
{ test: /\.txt$/, type: "asset/source" },
{ test: /\.txt$/, type: "asset/source" },
// { test: /\.css$/, use: ["style-loader", "css-loader"] },
{
test: /\.css$/,
use: [
{
loader: "style-loader",
// options: "",
},
{
loader: "css-loader",
options: {
importLoaders: 1,
},
},
{
loader: "postcss-loader",
// options: {
// postcssOptions: {
// plugins: [
// // require("autoprefixer"),
// require("postcss-preset-env"),
// ],
// },
// },
},
],
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"postcss-loader",
// {
// loader: "postcss-loader",
// options: {
// postcssOptions: {
// plugins: [
// // require("autoprefixer"),
// require("postcss-preset-env"),
// ],
// },
// },
// },
"less-loader",
],
},
// {
// test: /\.(png|svg|gif|jpe?g)$/,
// use: [
// {
// loader: "url-loader",
// options: {
// esModule: false, // 是否转化后的包裹成esModule
// // 修改打包的图片的名称
// // 占位符号
// // [ext]: 扩展名称
// // [name]:文件名称
// // [hash]: 文件内容产出的hash
// // [contentHash]: hash值
// // [hash:<length>] 截取hash 长度
// // [path]: 路径
// name: "[name].[hash:6].[ext]",
// // name: "img/[name].[hash:6].[ext]",
// outputPath: "img",
// limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
// },
// },
// ],
// },
{
test: /\.(png|svg|gif|jpe?g)$/,
type: "asset",
// 只需要经过asset 都打包到统一的目录下面
generator: {
filename: "img/[name].[hash:6][ext]",
},
parser: {
dataUrlCondition: {
maxSize: 25 * 1024,
},
},
},
{
test: /\.js/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
// options: {
// presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
// plugins: [],
// },
},
],
},
{
test: /\.ts$/,
use: ["babel-loader"],
},
],
},
plugins: [
new HtmlWbpackPlugin({
template: "./src/index.html",
title: "lalala",
}),
new CleanWebpackPlugin(),
// new DefinePlugin({
// BASE_URL: '"lalala"',
// }),
// new CopyWebpackPlugin({
// patterns: [
// { from: "public" },
// // { from: "other", to: "public" },
// ],
// }),
],
};
};
用到的依赖版本,有的依赖在实际项目中其实并不需要安装
"dependencies": {
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"autoprefixer": "^10.4.2",
"core-js": "^3.21.0",
"css-loader": "^5.2.6",
"html-webpack-plugin": "^5.3.1",
"less": "^4.1.1",
"less-loader": "^9.0.0",
"postcss-loader": "^6.2.1",
"regenerator-runtime": "^0.13.9",
"style-loader": "^2.0.0",
"ts-loader": "^9.2.6",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"devDependencies": {
"@babel/cli": "^7.17.0",
"@babel/core": "^7.17.2",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"babel-loader": "^8.2.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^9.0.0",
"file-loader": "^6.2.0",
"postcss": "^8.4.6",
"postcss-cli": "^9.1.0",
"postcss-preset-env": "^7.3.1",
"url-loader": "^4.1.1"
}
基础篇完结
原理篇
手写loader
Loader 本质上是一个函数, loader分类 pre前置. normal. inline post
我们先来看下loader的执行顺序, 一般我们是这么写loader
module: {
rules: [
{
test: /\.js$/,
use: [
// loader1, loader2, loaser3
{
loader: 'loader1',
options: {
name: 'xxxx',
age: 18
}
},
{
loader: 'bablelLoader',
options: {
presets: ["@babel/preset-env"]
}
}
]
}
]
}
解析到loader 是一个数组的时候,会从左往右解析,每一个loader 其实都有一个pitch 方法,解析到第一个loader的时候,会执行第一个loader的pitch 方法, 然后解析到第2个loader的时候,秽执行第二个loader的pitch 方法,解析完成后,loader 会开始从下往上,从右往左开始执行
// module.exports.pitch = () => {
// console.log(111);
// };
loader 有同步loader,也有异步loader, 可以同步执行,也可以异步执行
同步loader
// 写法一
module.exports = (content, map, meta) => {
console.log(content)
return content
}
// 写法二
module.exports = function(content, map, meta) {
this.callback(null, content, map, meta)
}
异步loader写法
module.exports = function(content, map, meta) {
const callback = this.async() // 在这里阻塞下一个loader执行,但不会阻塞其他操作,当调用callback 的时候,loader 才会往下执行
setTimeout(() => {
callback(null, content, map, meta)
}, 1000) // 一秒后loader 才会往下执行
}
我们来写一个简单的loader实现babel转换功能 ,里面用到loader-utils 来获取loader中的属性,用schema-utils 来验证loader传入参数的格式
const { getOptions } = require("loader-utils"); // 来获取loader传入的参数
const { validate } = require("schema-utils"); // 验证loader 传入参数的格式
const schema = require("./schema.json");
const babel = require("@babel/core");
const util = require("util");
module.exports = function(content, map, meta) {
const options = getOptions(this); // 拿到传给loader的参数 // this.getOptions()也可以获取
// 校验loader 参数
validate(schema, options, {
name: 'loader options 传入参数格式错误' // 校验失败提示信息
})
const callback = this.async(); // 会在这里阻塞,等到调用callback的时候,loader才会往下执行
// 使用bable 做编译
transform(content, options).then(({code, map}) => {
return callback(null, code, map, meta)
}).catch((e) => callback(e) )
setTimeout(() => {
callback(null, content, map, meta)
}, 1000) // 一秒后loader往下执行
}
schema.json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "描述, additionalProperties 表示能不能追加属性"
}
},
"additionalProperties": true
}
这样,我们就可以使用自己的loader了,不过要在使用的时候,将其导入进来
{
test: /\.js$/,
loader: path.resolve(__dirname, "loaders", "loader1"), // loaders 是放loader的文件夹,loader1是loader文件名
use: [
{
loader: "loader1",
options: {
name: "lalala",
age: 18,
},
},
]
}
手写plugin
plugin 是一个类,他利用tapable,里面提供了webpack生命周期不同时期的钩子函数(有同步钩子,异步钩子等),可以让plugin注册对应不同钩子的事件处理函数,然后在webpack 对应的生命周期的时候触发执行这些事件处理函数。
我们简单使用一个这个tapable 库
// npm i tapable -D
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook,
} = require("tapable");
// compiler 钩子,相当于生命周期函数,在触发compiler 的时候,会创建compildation
// compilation 钩子 ,其实是一个对象
class Lesson {
constructor() {
// 初始化hooks容器
this.hooks = {
// t同步勾子
go: new SyncHook(["address"]),
// go: new SyncBailHook(["address"]), // 一旦遇到return 就不会再执行,直接退出
// 异步并行勾子,里面并行执行
leave: new AsyncParallelHook(["name", "age"]),
// 异步串行 AsyncSeriesHook
// leave: new AsyncSeriesHook(["name", "age"]),
};
}
tap() {
// 往hooks容器中注册事件/添加回调函数
// 注册同步任务
this.hooks.go.tap("class0318", (address) => {
console.log("class0318", address);
});
this.hooks.go.tap("class0410", (address) => {
console.log("class0410", address);
});
// 注册异步任务
this.hooks.leave.tapAsync("class0510", (name, age, cb) => {
setTimeout(() => {
console.log("class0510", name, age);
cb();
}, 1000);
});
this.hooks.leave.tapPromise("class0610", (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("class0510", name, age);
resolve();
}, 1000);
});
});
}
start() {
// 触发hooks
this.hooks.go.call("c318");
// 触发异步勾子
this.hooks.leave.callAsync("lalla", 18, () => {
// 所有leavae 容器中的函数触发完了,才触发
console.log("end");
});
}
}
const l = new Lesson();
l.tap();
l.start(); // classo318 c318
// class0410 c318
Plugin1.js
class Plugin1 {
apply(complier) {
complier.hooks.emit.tap("plugin1", (compilation) => {
console.log("emit.tap");
});
// 异步串行勾子
complier.hooks.emit.tapAsync("plugin1", (compilation, cb) => {
setTimeout(() => {
console.log("emit.tapAsync");
cb(); // 一定要调用cb 才会执行
}, 1000);
});
complier.hooks.emit.tapPromise("plugin1", (compilation, cb) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("emit.tapPromise");
resolve(); // 一定要调用cb 才会执行
}, 1000);
});
});
complier.hooks.afterEmit.tap("plugin1", (compilation) => {
console.log("afterEmit.tap");
});
complier.hooks.done.tap("plugin1", (compilation) => {
console.log("done.tap");
});
}
}
module.exports = Plugin1;
Plugin2.js
// 利用node 开启调试模式
/**
* package.json 中开启 --inspect-brk 相当于首行开启一个断点
* “debugeStart”: "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
* 运行后,在要调试的地方打一个debugger
*/
const fs = require("fs");
const util = require("util");
const path = require("path");
// 快速创建一个基于webpack创建的文件类型
const webpack = require("webpack");
// RawSource 可以将读取文件的数据变成一个对象
const { RawSource } = webpack.sources;
// 将fs readFile 方法变成给予
// const readFile = util.promisify(fs.readFile);
class Plugin2 {
apply(compiler) {
// 在 thisCompilation 的时候,会初始化compilation
compiler.hooks.thisCompilation.tap("Plugin2", (compilation) => {
// debugger;
// console.log(compilation, "0000");
// 添加资源
compilation.hooks.additionalAssets.tapAsync("Plugin2", (cb) => {
// debugger;
// console.log(compilation, "0000");
const content = "hello word";
// 往输出的资源中,添加一个a.txt 文件
compilation.assets["a.txt"] = {
// 文件大小
size() {
return content.length;
},
// 文件内容
source() {
return content;
},
};
const data = fs.readFileSync(path.resolve(__dirname, "b.txt"));
console.log(data, "--->data");
compilation.assets["b.txt"] = new RawSource(data);
// 另一种输出资源的方式,相当于 compilation.assets["b.txt"] = new RawSource(data);
compilation.emitAsset("b.txt", new RawSource(data));
cb();
});
});
}
}
module.exports = Plugin2;
自己实现一个copyWebpackPlugin2
Schema.json
{
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"ignore": {
"type": "array"
}
},
"additionalProperties": false
}
copyWebpackPlugin2.js
const { promisify } = require("util");
const path = require("path");
const { validate } = require("schema-utils");
const fs = require("fs");
// 专门用来匹配文件列表
const globby = require("globby");
const readFiless = promisify(fs.readFile);
const schema = require("./schema.json");
// 快速创建一个基于webpack创建的文件类型
const webpack = require("webpack");
// RawSource 可以将读取文件的数据变成一个对象
const { RawSource } = webpack.sources;
// 将fs readFile 方法变成给予
// const readFile = util.promisify(fs.readFile);
class CopyWebpackPlugin2 {
constructor(options = {}) {
// 验证options 是否合格
validate(schema, options, {
name: "CopyWebpackPlugin2",
});
this.options = options;
}
apply(compiler) {
// 初始化compilation
compiler.hooks.thisCompilation.tap("CopyWebpackPlugin2", (compilation) => {
// 添加资源的hooks
compilation.hooks.additionalAssets.tapAsync(
"CopyWebpackPlugin2",
async (cb) => {
// 将from 中的资源复制到to 然后输出
// 读取from 资源,过滤掉ignore 生成webpack资源,然后输出
const { from, ignore } = this.options;
const to = this.options.to ? this.options.to : ".";
// 上下文地址 context 就是webpack配置 默认值是process.cwd
const context = compiler.options.context;
// 判断是不是绝对路径
const absoluteFrom = path.isAbsolute(from)
? from
: path.resolve(context, from);
// globby(要处理的文件夹, options)
const paths = await globby(absoluteFrom);
console.log(paths, "--->paths");
console.log(fs.readdirSync(paths[0]), "--->fs");
const pathss = fs.readdirSync(paths[0]);
// 读取paths 中所有资源
const files = await Promise.all(
pathss.map(async (abpath, index) => {
const relative = `${paths[0]}/${abpath}`;
console.log(relative, "--->relative");
const data = await fs.readFileSync(relative);
// const filename = path.basename(abpath);
// 和to 属性结合 没有to xx.js 有to to/xx.js
const filename = path.join(to, abpath);
return { data, filename };
})
);
// // 生成webpack 格式
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename,
};
});
assets.forEach((item) => {
compilation.emitAsset(item.filename, item.source);
});
cb();
}
);
});
}
}
module.exports = CopyWebpackPlugin2;
然后在配置文件中引入使用
const Plugin1 = require("./plugins/Plugin1");
const Plugin2 = require("./plugins/Plugin2");
const CopyWebpackPlugin2 = require("./plugins/CopyWebpackPlugin2");
plugins: [
new HtmlWbpackPlugin({
template: "./src/index.html",
title: "lalala",
}),
// new Plugin1(),
new Plugin2(),
new CopyWebpackPlugin2({
from: "public",
to: "css",
ignore: ["**/index.html"],
}),
],
手写webPack
-
webpack执行流程
引入webpack 配置文件,通过configFactory 来传入一个当前环境是dev还是pro,然后得到相应参数的webpack配置,最后会调用build方法,在bulid 方法内部会调用webpack ,并且传入一个webpack配置对象。得到一个compiler 对象,然后调用他的run 方法。执行
用webpack 初始化一个compiler对象。然后去运行编译,在webpack.config.js 的entry 中开始打包
打包的时候就会对文件进行处理利用loader 进行递归处理,直到所有的依赖都处理好,生成一副依赖图,编译模版,生成一个打包出来的chunk,然后再把每一个chunk根据依赖关系整合到输出列表中,然后通过fs将文件列表输出
首先我们在src下面新建一个lib文件夹,里面放置我们的webpack文件
新建一个index.js
const fs = require("fs");
const Compiler = require("./Compiler");
function mywebpack(config) {
return new Compiler(config);
}
module.exports = mywebpack;
然后新建Compiler.js。[这里查看完整代码](#### Compiler代码(TODO: 未写完)) // TODO: 这里代码是不完整的
const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
const { getAst, getCode, getDepes } = require("./parser");
class Compiler {
constructor(options = {}) {
this.options = options;
}
// 启动webpack 打包
run() {
// 1. 读取入口文件内容
const filePath = this.options.entry;
// const file = fs.readFileSync(filePath, "utf-8");
// // 第一步: 2. 将其解析成ast 抽象语法树
// const ast = babelParser.parse(file, {
// sourceType: "module", // 解析文件的模块化方案是 ES Module
// });
// // 获取到文件文件夹路径
// const dirname = path.dirname(filePath);
// // debugger;
// console.log(ast, "-->ast");
// // 收集依赖,可以利用ast 中的progress 中的body 属性type: "ImportDeclaration"来判断
// // 是不是属于文件引入,如果是,则找到他的source 中的value 拿到文件路径,这样比较麻烦
// // 所以利用babel 中的traverse 来快速搜集依赖 @babel/traverse
// // 定义存储依赖的容器
// const deps = {};
// // 第二步: 收集依赖
// traverse(ast, {
// // 内部会遍历ast 中program.body, 判断里面语句类型
// // 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
// ImportDeclaration(code) {
// // debugger;
// const { node } = code;
// // 文件的相对路径 '.add.js'
// const relativePath = node.source.value;
// // 根据入口文件生成基于入口文件的绝对路径
// const absolutePath = path.resolve(dirname, relativePath);
// // 添加依赖
// deps[relativePath] = absolutePath;
// },
// });
// console.log(deps);
// // 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
// const { code } = transformFromAst(ast, null, {
// presets: ["@babel/preset-env"],
// });
// console.log(code, "--->code");
// 1. 将文件解析为ast
const ast = getAst(filePath);
// 1. 获取ast 的依赖
const deps = getDepes(ast, filePath);
// 获取代码
const code = getCode(ast);
console.log(ast, deps, code, "--->");
}
}
module.exports = Compiler;
新建一个parser.js, 将代码解析文件抽取出来
const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
const parser = {
// 将文件解析为ast
getAst(filePath) {
// 读取文件
const file = fs.readFileSync(filePath, "utf-8");
// 第一步: 2. 将其解析成ast 抽象语法树
const ast = babelParser.parse(file, {
sourceType: "module", // 解析文件的模块化方案是 ES Module
});
return ast;
},
// 获取依赖
getDepes(ast, filePath) {
// 获取到文件文件夹路径
const dirname = path.dirname(filePath);
// 定义存储依赖的容器
const deps = {};
// 第二步: 收集依赖
traverse(ast, {
// 内部会遍历ast 中program.body, 判断里面语句类型
// 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
ImportDeclaration(code) {
// debugger;
const { node } = code;
// 文件的相对路径 '.add.js'
const relativePath = node.source.value;
// 根据入口文件生成基于入口文件的绝对路径
const absolutePath = path.resolve(dirname, relativePath);
// 添加依赖
deps[relativePath] = absolutePath;
},
});
return deps;
},
// ast解析成code
getCode(ast) {
// 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return code;
},
};
module.exports = parser;
在新建一个script文件夹,里面写入build.js,这样使用我们自己写的webpack进行打包
const mywebpack = require("../lib/mywebpack");
const config = require("../config/webpack.config");
const compiler = mywebpack(config);
// 开始打包
compiler.run();
在package.json中添加build命令
{
"name": "my_webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "node ./script/build.js",
"debug": "node --inspect-brk ./script/build.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/parser": "^7.17.3"
},
"dependencies": {
"@babel/traverse": "^7.17.3"
}
}
Compiler代码(TODO: 未写完)
const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
const { getAst, getCode, getDepes } = require("./parser");
class Compiler {
constructor(options = {}) {
// webpackConfig 的配置
this.options = options;
// 所有依赖容器
this.modules = [];
}
// 启动webpack 打包
run() {
// 1. 读取入口文件内容
const filePath = this.options.entry;
// const file = fs.readFileSync(filePath, "utf-8");
// // 第一步: 2. 将其解析成ast 抽象语法树
// const ast = babelParser.parse(file, {
// sourceType: "module", // 解析文件的模块化方案是 ES Module
// });
// // 获取到文件文件夹路径
// const dirname = path.dirname(filePath);
// // debugger;
// console.log(ast, "-->ast");
// // 收集依赖,可以利用ast 中的progress 中的body 属性type: "ImportDeclaration"来判断
// // 是不是属于文件引入,如果是,则找到他的source 中的value 拿到文件路径,这样比较麻烦
// // 所以利用babel 中的traverse 来快速搜集依赖 @babel/traverse
// // 定义存储依赖的容器
// const deps = {};
// // 第二步: 收集依赖
// traverse(ast, {
// // 内部会遍历ast 中program.body, 判断里面语句类型
// // 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
// ImportDeclaration(code) {
// // debugger;
// const { node } = code;
// // 文件的相对路径 '.add.js'
// const relativePath = node.source.value;
// // 根据入口文件生成基于入口文件的绝对路径
// const absolutePath = path.resolve(dirname, relativePath);
// // 添加依赖
// deps[relativePath] = absolutePath;
// },
// });
// console.log(deps);
// // 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
// const { code } = transformFromAst(ast, null, {
// presets: ["@babel/preset-env"],
// });
// console.log(code, "--->code");
// 第一次构建,得到入口文件信息
const fileInfo = this.build(filePath);
this.modules.push(fileInfo);
// 递归搜集依赖 遍历所有的依赖
this.modules.forEach((fileInfo) => {
// {'x.xx': 'xxx.xxx'}
// 取出当前文件所有依赖进行遍历
const deps = fileInfo.deps;
for (const relativePath in deps) {
// 得到当前依赖文件的绝对路径
const absolutePath = deps[relativePath];
// 对依赖文件进行处理
const fileInfo = this.build(absolutePath);
// 将后面的依赖push
this.modules.push(fileInfo);
}
});
// console.log(this.modules, "-->所有依赖");
// 将依赖整理成更好的依赖关系图
// const arr = {
// "index.js": {
// code: "xxx",
// deps: {
// "add.js": "xxx",
// },
// },
// "add.js": {
// code: "xxx",
// deps: {},
// },
// };
const depsGraph = this.modules.reduce((graph, module) => {
return {
...graph,
[module.filePath]: {
code: module.code,
deps: module.deps,
},
};
}, {});
// 根据依赖关系图生成打包资源
this.generate(depsGraph);
}
// 开始构建
build(filePath) {
// 1. 将文件解析为ast
const ast = getAst(filePath);
// 1. 获取ast 的依赖
const deps = getDepes(ast, filePath);
// 获取代码
const code = getCode(ast);
return {
// 当前模块文件路径
filePath,
// 当前文件所有依赖
deps,
// 当前文件解析后的代码
code,
};
}
// 构建输出资源方法
generate(depsGraph) {}
}
module.exports = Compiler;