webpack原理及基础配置:
Webpack 的核心原理是将项目中的所有文件视为模块,包括 JS 文件、CSS 文件、图片、字体文件等等。Webpack通过一个给定的主文件(如:index.js)开始找到项目的所有依赖文件,使用loaders处理它们,plugin可以压缩代码和图片,把所有依赖打包成一个 或多个bundle.js文件(捆bundle)浏览器可识别的JavaScript文件。
const path = require('path')
// 默认创建一个html文件,并自动将打包后的资源(js/css)引入
const HTMLWebpackPlugin = require('html-webpack-plugin')
// 删除上一次打包的文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 打包过后的css在js文件里,该插件可以把css单独抽出来,并自动引入index.html中(通过<link href="main.css" rel="stylesheet">引入)
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// webpack 中的所有的配置信息都写在 module.exports中
module.exports = {
mode: 'development',
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在的目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后的文件名
filename: 'bundle.js',
// 告诉webpack不适用箭头函数:因为webpack内部会使用箭头函数,不能使用babel转换他内部使用的东西
environment: {
arrowFunction: false
}
},
// 指定webpack打包时要使用的模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的laoder
use:[
// 配置babel
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets: {"chrome": "99", "ie": "11"},
// 指定corejs的版本:corejs作用就是
"corejs": "3",
// 使用corejs的方式:按需引入
"useBuiltIns": "usage"
}
]
]
}
},
// 将ts转为js
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
},
// style-loader 和 MiniCssExtractPlugin 不同之处在于 style-loader 在页面创建一个style标签,将样式插入该标签内
// MiniCssExtractPlugin 将打包后的css文件单独抽离出来,以<link href="xxx"></link>的形式引入页面,这个地方也可以用 style-loader 替换
// {test: /\.css$/i, use: ['style-loader', 'css-loader']}
{test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader']}
],
},
plugins: [
new HTMLWebpackPlugin({
// 根据该模板html生成一个html
template: "./src/index.html"
}),
// 清除上一次打包的文件
new CleanWebpackPlugin(),
new MiniCssExtractPlugin()
],
resolve: {
// ts和js扩展名都将作为模块,但是注意引入模块的时候不要写.ts
extensions: [".ts", ".js"]
}
}
注意:以下资源都是用webpack中的none模式打包便于查看打包后的文件
loaders(加载器)
Webpack允许使用加载器来预处理文件,从而将资源模块转换成js代码。这允许你绑定JavaScript之外的任何静态资源。例如我们可以使用css文件,通过loaders最终被编为js代码。
css-loader
配置:
module: {
rules: [
{test: /.css$/, use: ['style-loader', 'css-loader']}
]
}
我们先要通过import()在js文件中引入css建立依赖关系
css-loader会将css文件编译成js代码:我们可以再打包后的文件中看到:
___CSS_LOADER_EXPORT___.push([module.id, `p {
background-color: red;
}`, ""]);
style-loader
打包后我们可以看到bundle.js文件中有个insertStyleElement函数:创建一个style标签
/***/ ((module) => {
/* istanbul ignore next */
function insertStyleElement(options) {
var element = document.createElement("style");
options.setAttributes(element, options.attributes);
options.insert(element, options.options);
return element;
}
module.exports = insertStyleElement;
/***/ }),
file-loader
配置:
module: {
rules: [
{test: /.jpg$/, use: 'file-loader'}
]
}
加载图片、字体等资源文件用法跟上面的loader一样在配置项中的modules.rules中配置,打包后的部分代码如下:
//默认导出该资源模块
const __WEBPACK_DEFAULT_EXPORT__ = (__webpack_require__.p + "d903073b89d0bb6d395f96fea59059e2.jpg");
// 导入该资源模块
var _tup_jpg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
url-loader
配置:
module: {
rules: [
{test: /.jpg$/, loader: 'url-loader', options: {limit: 10 *1024}} // 限制大小10kb
]
}
使用base64编码资源文件,这种loader适合处理小文件,以减少向服务器请求的次数,大文件还是需要file-loader处理,所以url-loader要分file-loader搭配使用。
打包后的代码:就是将文件转换成base64了。
注意:当我们需要打包css文件中的资源文件时例如我们用background-image: url(tup.jpg);方式引入了一个图片,由于webpack5 file-loader和url-loader被弃用,我们需要向下面这样配置:
{
test: /.jpg$/,
use: {
loader: 'url-loader',
options: { limit: 3 * 1024, esModule: false},
},
type: "javascript/auto"
},
或者我们可以使用官方推荐的 asset module 来加载资源文件,也可以达到上面的效果用法如下:
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
babel-loader
注意:babel-loader还需要配合其他的包来使用:
npm install -D babel-loader @babel/core @babel/preset-env
- babel-loader:babel解析es6+语法的桥梁
- @babel/core:babel-core 的作用是把 js 代码分析成ast (抽象语法树) ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js
- @babel/preset-env:一组插件的集合,用于转换es6+等最新语法
配置:
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}}
]
}
html-loader
前面我们介绍了css中和js中加载资源模块的方式,下面我们介绍html中加载资源模块的方式,当然我们需要在我们的代码中通过import引入html文件,如下:
// footer.html中
<footer>
<img src="tup.jpg" alt="better" width="156">
<a href="tup.jpg">dovnasdf</a>
</footer>
// index.js中
import htmlStr from './footer.html'
document.write(htmlStr)
// webpack.config.js 中在module.rules中
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
sources: {
list: [
{
tag: 'img',
attribute: 'src',
type: 'src',
},
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
}
}
根据官网我们可以不设置sources为true(默认值),默认情况下,每个可加载属性都将被导入,但是a标签的href不会被导入。所以我们单独设置。
官网链接:https://webpack.js.org/loaders/html-loader/#root
开发一个简易的loader
Loader 本质上是导出为函数的 JavaScript 模块。它接收资源文件或者上一个 Loader 产生的结果作为入参,也可以用多个 Loader 函数组成 loader chain(链),最终输出转换后的结果。
我们可以自己开发一个markdown-loader用来处理.md文件将其转化为html
// markdown-loader.js中
const marked = require('marked')
module.exports = function (source) {
// source是匹配到的资源
let res = marked.parse(source)
// 返回的结果会交给html-loader处理
return res
};
// webpack.config.js中module.rules中的配置
{
test: /.md$/,
// 此处我们写的是loader的相对路径也可以
use:['html-loader', './src/markdown-loader.js']
},
plugins
Plugin 是用来扩展 Webpack 功能的,通过在构建流程里注入钩子实现,它给 Webpack 带来了很大的灵活性。 Webpack 基于观察者模式,在运行的生命周期中会广播出许多事件,Plugin 通过监听这些事件,就可以在特定的阶段执行自己的插件任务,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
上面代码中我们介绍了一些常用的plugins,我们这里就不在介绍了。
实现一个plugin
我们实现一个去掉打包后的js文件的中的注释
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', compilation => {
// complilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
// name 就是每次打包后的文件名称
if (name.endsWith('.js')) {
// 通过source拿到文件的内容
const contents = compilation.assets[name].source()
// 替换注释
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments, // 返回修改后的内容
size: () => withoutComments.length // 返回内容的大小
}
}
}
})
}
}
Source Map
Source Map 就是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系。在开发模式下默认是启用了Source Map的,但是代码出错后定位到的位置不是源代码的位置,而是打包后的文件位置,所以我们需要手动更改Source Map的模式
配置:
// 定位到行数和列数的同时,展示具体报错的源码
devtool: 'source-map'
// devtool: 'eval-source-map'
当我们配置后在打包目录中会出现一个.map文件,这个文件就是Source Map文件。打包后的bundle.js文件后面会有一个://# sourceMappingURL=bundle.js.map 注释,就是该注释标记了Source Map的位置,浏览器根据这个才能找到源代码的位置。
配置:
devtool: 'nosources-source-map' // 只暴露出错的行号,不展示源码
development模式下建议使用source-map,production环境下不建议开启,或者使用nosources-source-map。当然不仅仅只有这几种模式
不同环境中的webpack的配置
使用merge函数
首先要安装 webpack-merge 这个包,用于然后分别创建
webpack.common.js:用于设置公共的配置
webpack.dev.js:用于设置开发环境下的配置
webpack.prod.js:用于设置生产环境下的配置
// webpack.common.js
const path = require('path')
const common = require('./webpack.common')
const {merge} = require('webpack-merge')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = merge(common, {
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist1'),
filename: 'bundle1.js'
},
plugins: [
new CleanWebpackPlugin()
]
})
// webpack.prod.js
// 导入公共的webpack配置
const common = require('./webpack.common')
const {merge} = require('webpack-merge')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 使用merge函数合并公共和生产环境的配置
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({patterns: [
{ from: "public", to: "" },
]})
]
})
// webpack.dev.js
const common = require('./webpack.common')
const {merge} = require('webpack-merge')
module.exports = merge(common, {
mode: 'development',
})
module.exports导出函数的形式
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env, argv) => {
// config为development模式下的配置
const config = {
mode: 'development',
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle1.js',
environment: {
arrowFunction: false
}
},
module: {
rules: [
{test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader']}
],
},
plugins: [
new HTMLWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin()
]
}
if(env.production) {
// production环境下
config.mode = 'production'
config.devtool = 'nosources-source-map'
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin({patterns: [
{ from: "public", to: "" },
]})
]
}
// 返回该配置
return config
}
DefinePlugin
DefinePlugin 允许在 编译时 将你代码中的变量替换为其他值或表达式。这在需要根据开发模式与生产模式进行不同的操作时,非常有用。例如,在不同环境下我们需要设置不同的api地址。
使用:
plugins: [
new webpack.DefinePlugin({
'process.env.BASE_URL': JSON.stringify('https://www.xx.com')
})
]
代码分离
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后便能按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。
目录结构(包括打包后的结果)
|- package.json
|- yarn.lock
|- webpack.config.js
|- /dist
|- useCommon1-bundle.js
|- useCommon1.html
|- useCommon2-bundle.js
|- useCommon2.html
|- /src
|- common.js
|- useCommon1.js
|- useCommon1.html
|- useCommon1.css
|- useCommon2.js
|- useCommon2.html
|- useCommon2.css
其中我们会useCommon1.js和useCommon2.js中引入common.js模块并分别引入这两个模块对应的css,dist目录里面还会有两个css文件我们暂时先不说明,最后会细说。
wepack.config.js的配置:
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
// 配置多入口
useCommon1: './src/useCommon1.js',
useCommon2: './src/useCommon2.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// 输出的文件名,name为entry配置的key
filename: '[name]-bundle.js'
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HTMLWebpackPlugin({
template: './src/useCommon1.html',
filename: 'useCommon1.html'
}),
new HTMLWebpackPlugin({
template: './src/useCommon2.html',
filename: 'useCommon2.html'
})
]
}
观察打包后的useCommon1-bundle.js和useCommon2-bundle.js中我们发现都有共同的模块common.js中的内容,所以这种方式存在弊端:
- 如果入口 chunk 之间包含一些重复的模块,那么这些重复模块会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能动态地拆分应用程序逻辑中的核心代码。
所以我们可以将公共的模块提取出来,配置如下:
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'development',
entry: {
// 配置多入口
useCommon1: {
import: './src/useCommon1.js',
dependOn: 'shared'
},
useCommon2: {
import: './src/useCommon2.js',
dependOn: 'shared'
},
shared: './src/common.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// 输出的文件名,name为entry配置的key
filename: '[name]-bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HTMLWebpackPlugin({
template: './src/useCommon1.html',
filename: 'useCommon1.html',
// 因为该插件会注入所有打包结果的js文件,所以我们设置一个chunks单独引入某一个打包结果
chunks: ['useCommon1']
}),
new HTMLWebpackPlugin({
template: './src/useCommon2.html',
filename: 'useCommon2.html',
chunks: ['useCommon2']
})
]
}
此时我们打包后会出现一个单独的公共模块:shared-bundle.js
关于上面的css样式问题,因为我们需要在两个不同的.html文件中引入不同的css,所以我们不能使用style-loader(踩了很长时间的坑),因为我们使用了多个入口点,会导致样式文件在页面中的加载和注入方式发生变化。所以我建议使用使用 MiniCssExtractPlugin 插件具体配置如下:
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}