目录结构
dir.pngpaths 配置
// path 与 fs 是 node 环境自带不需要通过 npm 安装
const path = require('path')
const fs = require('fs')
// 获取项目根目录路径
const appDirectory = fs.realpathSync(path.resolve(__dirname, '../'))
// 获取目标文件的函数
const resolveApp = relativePath => path.resolve(appDirectory, relativePath)
module.exports = {
appPath: resolveApp('.'),
appScript: resolveApp('script'),
appSrc: resolveApp('src'),
appDist: resolveApp('dist'),
appPublic: resolveApp('public'),
appIndexJs: resolveApp('src/index.js'),
appIndexHtml: resolveApp('public/index.html')
}
实现最基本的打包功能
// webpack.dev.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const paths = require('../paths')
const config = {
// mode 可选参数 development | production | none
// 开启 webpack 开发环境下内置插件优化
mode: 'development',
// 主入口文件
// webpack 会根据主入口文件中的 import 或者 require 的语句 先解析推断入口文件所依赖资源模块 然后再去解析每个资源模块的依赖 最终形成一个依赖关系树
entry: paths.appIndexJs,
// 输出目录
// 开发环境下并不会产生 dist 到磁盘目录 而是存储在内存中
output: {
// 编译后的的资源引用地址的前缀都会加上 publicPath
// webpack-dev-server 会在 publicPath 下启用服务来访问 webpack 输出的文件
publicPath: '/',
// 输出目录路径
path: paths.appDist,
// 最终输出的文件名
filename: 'js/[name].bundle.js',
},
resolve: {
// 模块引入如果不加后缀名 根据以下后缀名顺序查找
extensions: ['.js', '.jsx']
},
// 模块配置
// webpack 会循环遍历上面提到的依赖树 根据下面 rule 的配置 找到对应的模块资源文件 交给对应的 loader 加载 最终把加载的结果打包到 bundle.js 中
module: {
rules: [
{
// 匹配后缀为 .js|.jsx 的模块
test: /\.jsx?$/,
// 需要排除目录 接受正则匹配与绝对路径
exclude: /node_modules/,
use: [
{
// 通过 babel 对源代码中的 后缀为 .js|.jsx的语法 进行转换(转换配置可写在根目录下的 .bablrc文件中)
loader: 'babel-loader',
options: {
// 开启 babel-loader 加载结果的缓存
// 指定的目录将用来缓存 loader 的加载结果 设置空值或者 true 的话 使用默认缓存目录 node_modules/.cache/babel-loader
cacheDirectory: true
}
}
],
},
// 匹配后缀名为 .less 的模块
{
test: /\.less/,
// loader 加载资源模块 类似工作管道 可以使用多个 loader 加载同一个资源模块 最终返回一段标准的 js 代码字符串(webpack 默认只支持 js 的语法)
// 多个 loader 执行顺序是从后往前执行
use: [
// 把 css-loader 的处理结果 通过 style 标签 添加到 html 页面
'style-loader',
{
loader: 'css-loader',
options: {
// 此处开启 在他前面的所有 loader 都必须开启
sourceMap: true,
// 在当前 loader 加载之前的 loader 数量
importLoaders: 1
}
},
{
// 把 .less 模块的内容加载成 css
loader: 'less-loader',
options: {
// 开启源码地图(可理解成源码中的错误定位)
sourceMap: true
}
}
]
},
// 匹配图片资源模块
{
test: /\.(png|jpg|jpeg|gif)/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
// 打包到 ouput.path + images 目录下
outputPath: 'images',
}
}
]
}
]
},
// 开发服务 这里使用的是 webpack-dev-server
devServer: {
// 页面刷新出现 404 重新定向到 output.path 下的index.html
historyApiFallback: true,
// 告诉服务器从哪个目录中提供内容
contentBase: paths.appDist,
// 服务端口
port: 8080,
// webpack 打包完成 自动打开浏览器
open: true,
},
plugins: [
// 首次打包的时候清除 output.path 目录中所有的文件 保留 ouput.path 文件夹 并且再每次 rebuild 的时候会清除所有不再被使用的文件
new CleanWebpackPlugin(),
// 自动生成 html 页面 并把打包后的资源内容 自动添加到页面上
new HtmlWebpackPlugin({
// 指定 html 模板路径
template: paths.appIndexHtml,
// 打包后的文件名
filename: 'index.html'
})
],
}
module.exports = config
.babelrc 基本配置
@babel/preset-env
在 babel 中 preset 表示 plugin 的集合, @babel/preset-env 可以让 babel 根据我们配置的 Browserslist 按需转换语法和添加 polyfill
配置 Browserslist)
Browserslist
配置有三种方式 并按下面的优先级使用:
1. @babel/preset-env options.targets
2. package.json 中的 browserslist
3. .browserslistrc 配置文件
--useBuiltIns(false | entry | usage)
默认值为false babel 只对新的语法如 class|箭头函数进行语法转换 对于 Object.assign|Promise|Set| Proxy 等新的 API 不进行转换 保留原样输出 如需使用 则需要添加 polyfill
false
// .babelrc
{
presets: [
[
"@babel/preset-env",
{
"useBuiltIns": false
}
]
]
}
此时不对 polyfill 做操作 如果引入 @babel/polyfill 则无视 Browserslist 配置的 引入所有的 polyfill
entry
// .babelrc
{
presets: [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3
}
]
]
}
根据 Browserslist 配置 引入浏览器不兼容的 polyfill 需要在入口文件手动添加
import 'core-js/stable' & import 'regenerator-runtime/runtime'
usage
// .babelrc
{
presets: [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
根据 Browserslist 的配置,以及你代码中用到的 API 来自动添加 polyfill,实现了按需添加
@babel/plugin-transform-plugin
通过 babel 转换后的代码 会有有一些辅助代码内嵌在模块当中 如果是多个模块的话 就会有大量的重复代码 这个时候 就需要通过 @babel/plugin-transform-runtime 插件复用 babel 转换后的辅助函数
@babel/preset-react
支持 react 语法转换的插件集合
@babel/plugin-proposal-decorators
支持 装饰器语法 主要用于 react 的高阶组件
@babel/plugin-proposal-class-properties
支持类内部属性直接赋值
.babelrc
// .babelrc
{
presets: [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react"
],
plugins: [
"@babel/plugin-transform-plugin",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
}
后续还会加上 react-hot-loader(热模块更新的配置) babel-plugin-import 集成 antd 按需加载的配置
package.json
{
...
"script": {
"start": "webpack-dev-server --config script/config/webpack.dev.js"
}
}
至此一个简配版的 webpack 配置完成 运行 npm start 就可以启动项目了
打包动态链接库(DLLPlugin&DLLReference)
根据简配版的配置 我们会把第三方依赖也打包进我们的 bundle.js 这样会导致我们最终的 bundle.js 体积过大 这时候我们就该考虑把这些第三方依赖单独拆分出来
DllPlugin 可以帮助我们把不需要频繁更新的第三方依赖进行编译 单独打包成一个 bundle 并输出一份依赖映射文件 manifest.json
DllReferencePlugin 解析 manifest.json 映射文件 就可以引用我们打包后的依赖了
需要在 script/config 目录下重新新建一个 webpack.config.dll.js 的文件 专门用来打包动态链接库
// webpack.config.dll.js
const webpack = require('webpack')
const paths = require('../paths')
const config = {
mode: 'production',
entry: {
react: ['react', 'react-dom']
},
output: {
path: paths.appDist,
filename: 'dll/[name].dll.[contenthash:8].bundle.js',
// 暴露给外部使用的库的名称
library: '[name]_dll'
},
plugins: [
new webpack.DllPlugin({
// 就是我们需要引用库的名称 (这里必须与 library 一致)
name: '[name]_dll',
path: `${paths.appDist}/dll/manifest.json`
})
]
}
module.exports = config
在 package.json scripts 中增加脚本命令
{
....,
"scripts": {
...,
"build:dll": "webpack --config script/config/webpack.config.dll.js"
}
}
执行 npm run build:dll,可以看到 dist 目录如增加了 dll 目录,之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。
修改 webpack.dev.js 中的配置
{
...,
plugins: [
cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'], //不删除dll目录
new webpack.DllReferencePlugin({
manifest: `${paths.appDist}/dll/manifest.json`
})
]
}
public/index.html 添加 script 标签引用我们打包的动态链接库
<script src="./dll/react.dll.cb13c2ef.bundle.js"></script>
抽离公共代码
抽离公共代码是对于多页应用来说的(即多入口配置),如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。
// webpack.dev.js
{
...,
optimization: {
splitChunks: {
// 可选参数 initial|async|all
// initial 入口引入 对于异步引入的模块不抽离
// async 异步引入 只对异步引入的模块做抽离
// all 包含以上两种
chunks: "all",
cacheGroups: {
// 第三方依赖提取
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 优先级 根据优先级高的优先抽离
test: /node_modules/, // 匹配 node_modules 下引用的模块
minSize: 0, // 模块的最小体积规则 高于这个做抽离
minChunks: 1 // 模块最少引用次数
},
// 公用模块提取
common: {
name: 'common',
priority: 0,
minSize: 0,
minChunks: 2
}
}
}
}
}
HMR(热模块跟新)
我们不是使用了 webpack-dev-server 可以自动刷新了吗?为什么还需要热模块更新呢?举个🌰 比如我们页面当中有个表单我们把数据都填好了 但是我们发现 提交按钮没加 我们去代码里加一下提交按钮组件 保存代码 回到浏览器 此时页面状态被刷新了 所有填写的数据都没了 你有需要重新填写一遍
如果使用的是 HMR 就可以实现只将修改的模块实时替换至应用中 不必完全刷新整个应用了
// webpack.dev.js
{
devServer: {
...,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
还需要安装 react-hot-loader
npm i react-hot-loader -D
// .babelrc
{
"plugins": [
...,
"react-hot-loader/babel"
]
}
// webpack.dev.js
{
...,
entry: ['react-hot-loader/patch', paths.appIndexJs]
}
// App.js
import { hot } from 'react-hot-loader/root'
...
export default module.hot ? hot(App) : App
module.hot 是根据你 webpack 是否开启了 devServer.hot
好了 Bye Bye
网友评论