本文内容覆盖以下方面:
- 介绍npm包的发布流程。
- 教你如何搭建一个简单的webpack5项目。
- 配置一个ts + react18项目脚手架。
- 如何分析并优化webpack配置。
- 代码规范化,提交规范化
- 手写loader、plugin等。
脚手架代码已经提交到github,有需要可以下载学习:react-webpack-staging
1. 发布一个npm包
1.1 初始化npm
npm init
1.2 安装依赖
将镜像源更改为淘宝镜像。然后,在全局和开发环境都安装webpack
npm config set registry https://registry.npm.taobao.org
sudo npm i -g webpack webpack-cli
npm i -D webpack webpack-cli
1.3 写一个简单demo
const heloword = () => {
console.log('Hello World!')
}
heloword();
exports.heloword = heloword;
运行下看看效果
1.4 创建一个账号
npm adduser
发现镜像不能直接发布
所以还是需要切换到官方地址
npm config set registry https://registry.npmjs.org
输入相关信息,npm会给你发送验证邮件,输入邮件中的验证码就完成登录了。
这里管理npm源也可使用nrm。
sudo npm install -g nrm
nrm use npm # 切换为npm官方
nrm use taobao # 切换为淘宝镜像
1.5 发布第一个版本
执行下面命令就可以发布
npm publish
我们到npm官网搜一下 www.npmjs.com/search?q=re…
确实发布了,很好!
1.6 引用自己的包
const { heloword } = require('react-webpack-staging');
heloword();
可以验证是没有问题的
1.7 编译
- 执行编译命令:
webpack ./index.js
- 发现报错了,没有指定环境变量。所以加上环境变量
webpack --mode=development ./index.js
- 把命令放在scripts中,则变成这样:
"scripts": {
"build": "webpack --config=webpack.config.js"
}
- 可以看看编译出来的产物是这样:
1.8 其他
设置上传到npm的白名单
"files": [
"dist"
],
2. webpack配置
2.1 基本配置
我们将上述过程调整下目录结构,并把配置统一放在webpack.config.js中。
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, './src/index.js'),
output: {
filename: 'main.js',
path: path.resolve(__dirname, './dist')
}
}
package.json
"scripts": {
"build": "webpack --config=webpack.config.js"
}
重新运行命令,并得到产出物
npm run build
2.2 html
2.2.1 html模版中引用JS脚本
我们需要在html模版中引用JS脚本,为了让每次生成的脚步不一样让html识别js不一样时重新加载需要给每次生成的js脚步加上hash码。
脚本hash配置
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, './dist')
},
html插件安装
sudo npm i -D html-webpack-plugin
创建html文件 public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>webpack react脚手架</title>
</head>
<body></body>
</html>
html插件配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname, './public/index.html')
})
]
打开这个文件并查看控制台,可以看到这样的效果
2.2.2 清理之前打包的文件
我们发现上面每次执行都生成了一个重复的配置文件,所以可以安装下面这个插件来保证只保留最新的js文件。
sudo npm i clean-webpack-plugin -D
配置
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
]
验证,编译后发现文件确实只有最新的了
2.2.3 多入口
用多个HtmlWebpackPlugin配置每个页码所用的脚本即可:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
page1: path.resolve(__dirname, './src/page1.js'),
page2: path.resolve(__dirname, './src/page2.js')
},
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, './dist')
},
plugins:[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/page1.html'),
filename: 'page1.html',
chunks: ['page1']
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/page2.html'),
filename: 'page2.html',
chunks: ['page2']
})
]
}
2.3 css
2.3.1 加载css/less文件
安装相关的loader
npm i -D style-loader css-loader less less-loader
配置
module.exports = {
...
module: {
rules: [{
test: /\.css$/,// css后缀文件
use: ['style-loader', 'css-loader']// 从右向左解析原则
}, {
test: /\.less$/,// css后缀文件
use: ['style-loader', 'css-loader', 'less-loader']// 从右向左解析原则
}]
}
}
编译
验证
打开page1.html可以看到效果:
2.3.2 拆分CSS
npm i -D mini-css-extract-plugin
配置
...
plugins:[
...
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].css'
})
],
module: {
...
rules: [{
test: /\.less$/,// css后缀文件
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']// 从右向左解析原则
}]
}
需要注意的是,要去掉'style-loader'会与mini-css-extract-plugin冲突。
ReferenceError: document is not defined
看下编译结果
2.3.3 添加浏览器前缀
npm i -D autoprefixer postcss-preset-env
配置
module: {
rules: [{
test: /\.less$/,// css后缀文件
use: [
...
{
loader:'postcss-loader',
// 配置参数
options:{
postcssOptions:{
// 添加前缀
plugins:[
require('autoprefixer'),
require('postcss-preset-env')
]
}
}
}
]
}]
}
当然也可以加到postcss.config.js文件,配置如下:
module.exports = {
plugins: [require('autoprefixer')]
}
这里为了减少配置文件,不使用这种方式。
编译后发现没有生效,查看文档发现需要配置兼容的浏览器环境
{
loader:'postcss-loader',
// 配置参数
options:{
postcssOptions:{
// 添加前缀
plugins:[
require('autoprefixer')({
overrideBrowserslist: [
"last 2 version",
"> 1%",
"iOS >= 7",
"Android > 4.1",
"Firefox > 20"
]
}),
require('postcss-preset-env')
]
}
}
},
当然也可以在package.json中加browserslist,也可以用browserslistrc配置文件。这两种都不推荐。
验证下效果:
2.4 文件加载
文件、图片、字体、媒体文件等。
module.exports = {
module: {
rules: [{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
}]
}
}
TODO 验证
2.5 babel转义js
npm i -D url-loader file-loader
module.exports = {
module: {
rules: [{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
exclude: /node_modules/
}]
}
}
babel-loader只会将 ES6/7/8语法转换为ES5语法,但是对新api并不会转换 例如(promise、Generator、Set、Maps、Proxy等)
此时我们需要借助babel-polyfill来帮助我们转换
npm i -D @babel/polyfill
module.exports = {
entry: {
page1: ["@babel/polyfill", path.resolve(__dirname, './src/page1.js')],
page2: ["@babel/polyfill", path.resolve(__dirname, './src/page2.js')]
}
}
2.6 开发服务器
安装插件webpack-dev-server
npm i -D webpack-dev-server
配置
webpack.config.js
const Webpack = require('webpack')
module.exports = {
...
devServer: {
port: 3000,
hot: true,
contentBase: '../dist'
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}
pageckage.json
"scripts": {
"build": "webpack --config=webpack.config.js",
"dev": "webpack-dev-server --config=webpack.config.js --open"
},
验证下,保存后重新刷新
3. react项目
3.1 基本配置
安装react依赖
tnpm i --save react react-dom
配置@babel/preset-react解析react语法。
webpack.config.js
module.exports = {
...
module: {
rules: [{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
'@babel/preset-react', // 支持react
'@babel/preset-env'
]
}
},
exclude: /node_modules/
},
]}
}
改造下入口文件page2.js
import './page2.less';
import React from 'react';
import ReactDOM from 'react-dom';
import Container from './components/Container.js'
ReactDOM.render(
<Container />,
document.getElementById('root')
);
改造下组件components/Container.js
import React from 'react';
const Component = () => {
return <div class="container">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
}
export default Component;
验证下能跑起来。
3.2 区分环境
安装依赖
npm i -D webpack-merge mini-css-extract-plugin css-minimizer-webpack-plugin
先分一下文件:
基本配置webpack.base.js。
- 注意,为了使修改文件仅对部分文件生效,对其中的js使用chunkhash,内容使用contenthash。
- 用环境变量process.argv判断是什么环境,从而决定是否做一些压缩等处理。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
...
output: {
filename: '[name].[chunkhash:8].js',
path: path.resolve(__dirname, '../dist')
},
plugins:[
...
new MiniCssExtractPlugin({
filename: devMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: devMode ? '[id].css' : '[id].[contenthash].css'
})
],
module: {
rules: [
...
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[contenthash:8].[ext]'
}
}
}
}
]
}
]}
}
webpack.dev.js
const path = require('path');
const BaseConfig = require('./webpack.base.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge.merge(BaseConfig,{
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
port: 3000,
hot: true,
static: {
directory: path.join(__dirname, '../dist'),
},
compress: true
}
});
webpack.dev.js
const BaseConfig = require('./webpack.base.js')
const WebpackMerge = require('webpack-merge')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = WebpackMerge.merge(BaseConfig, {
mode: 'production',
devtool: 'eval-cheap-module-source-map',
optimization: {
minimizer: [
new CssMinimizerPlugin({})
],
splitChunks:{
chunks: 'all',
cacheGroups: {
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // 只打包初始时依赖的第三方
}
}
}
}
})
这里顺便介绍下devtool的配置。
开发过程中或者打包后的代码都是webpack处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map就是用来做源码映射的,
不同的映射模式会明显影响到构建和重新构建的速度, devtool选项就是webpack提供的选择源码映射方式的配置。
devtool的命名规则为 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
| 关键字 | 描述 |
|---|---|
| inline | 代码内通过 dataUrl 形式引入 SourceMap |
| hidden | 生成 SourceMap 文件,但不使用 |
| eval | eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap |
| nosources | 不生成 SourceMap |
| cheap | 只需要定位到行信息,不需要列信息 |
| module | 展示源代码中的错误位置 |
开发环境推荐:eval-cheap-module-source-map
- 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
- 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
- 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
后续还会介绍如何进行优化。
3.3 TS配置
npm i -D typescript ts-loader @babel/preset-typescript @babel/plugin-transform-runtime
- 文件后缀名都改成tsx
webpack.base.js
- 初始化ts配置并修改配置
tsc --init
- 解决类型报错
此时文件内容还会报类型错误,安装下面类型即可
npm i -D @types/react-dom @types/react
- 修改扩展名extensions
3.4 支持注解
npm i @babel/plugin-proposal-decorators -D
开启ts注解配置
{
"experimentalDecorators": true,
}
TODO 验证
3.5 支持热更新
目的:修改tsx文件,能够带状态刷新(hooks除外)
npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = WebpackMerge.merge(BaseConfig,{
plugins: [
new ReactRefreshWebpackPlugin(), // 添加热更新插件
]
});
TODO 验证
4. 配置优化
4.1 打包性能分析
安装以下 webpack 插件,帮助我们分析优化效率:
- progress-bar-webpack-plugin:查看编译进度;
- speed-measure-webpack-plugin:查看编译速度;
- webpack-bundle-analyzer:打包体积分析。
npm i -D progress-bar-webpack-plugin speed-measure-webpack-plugin webpack-bundle-analyzer chalk@4.x
- 进度条
webpack.prod.js
module.exports = WebpackMerge.merge(BaseConfig, {
...
plugins: [// 进度条
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
})
]
}
包含内容、进度条、进度百分比、消耗时间,进度条效果如下:
- 编译速度
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...webpack config...
});
包含各工具的构建耗时,效果如下:
- 包体积分析
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
//...
plugins: [
// 打包体积分析
new BundleAnalyzerPlugin(),
]
};
运行npm run build后能看到会自动打开http://127.0.0.1:8888/展示包的分布
4.2 文件缓存
webpack5 开箱即用的持久缓存是比 dll 更优的解决方案。
配置
module.exports = {
mode: 'development',
cache: {
type: "filesystem", // 使用文件缓存
}
}
可见首次构建耗时提升到了3792ms。但随后的编译耗时从1920ms降到了576ms。效果十分明显
4.3 其他尝试
- include/exclude
配置文件范围,有略微提升,不是很稳定。目前项目文件较少,此效果可以忽略。
- asset/resource
使用 webpack 资源模块 (asset module) 代替旧的 assets loader(如
file-loader/url-loader/raw-loader 等),减少 loader 配置数量。
目前项目文件较少,此效果可以忽略。
- thread-loader
npm i -D thread-loader
目前项目文件较少,此效果可以忽略。
4.4 terser/gzip等优化体积
优化前
- 使用terser-webpack-plugin压缩
webpack.prod.js
const TerserPlugin = require("terser-webpack-plugin");
{
mode: 'production',
optimization: {
...
minimizer: [
new CssMinimizerPlugin({}),
new TerserPlugin({
parallel: 4,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
]
}
}
减少200k
- gzip压缩
npm i compression-webpack-plugin glob -D
const BaseConfig = require('./webpack.base.js')
const WebpackMerge = require('webpack-merge')
const CompressionPlugin = require('compression-webpack-plugin');
const config = WebpackMerge.merge(BaseConfig, {
mode: 'production',
plugins: [new CompressionPlugin({
test: /.(js|css)$/, // 只生成css,js压缩文件
filename: '[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式,默认是gzip
test: /.(js|css)$/, // 只生成css,js压缩文件
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8 // 压缩率,默认值是 0.8
})],
}
可见又缩小到之前的1/3
4.5 代码分离
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。
- 抽离重复代码
SplitChunksPlugin 插件开箱即用,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
webpack 将根据以下条件自动拆分 chunks:
新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹; 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积); 当按需加载 chunks 时,并行请求的最大数量小于或等于 30; 当加载初始化页面时,并发请求的最大数量小于或等于 30; 通过 splitChunks 把 react 等公共库抽离出来,不重复引入占用体积。
此配置已有,略
- CSS 文件分离
MiniCssExtractPlugin 插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
此配置已有,略
- 代码分割第三方包和公共模块
一般第三方包的代码变化频率比较小,可以单独把node_modules中的代码单独打包, 当第三包代码没变化时,对应chunkhash值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积
webpack.prod.js
{
splitChunks:{
chunks: 'all',
cacheGroups: {
vendors: { // 提取node_modules代码
test: /[\\/]node_modules[\\/]/,
name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
commons: { // 提取页面公共代码
name: 'commons', // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
}
}
}
}
4.6 配置alias别名
webpack支持设置别名alias,设置别名可以让后续引用的地方减少路径的复杂度。
webpack.base.js
module.exports = {
resolve: {
alias: {
'@': path.join(__dirname, '../src')
}
},
}
tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}
配置修改完成后,在项目中使用 @/xxx.xx,就会指向项目中src/xxx.xx,在js/ts文件和css文件中都可以用。
4.7 配置包查找范围
使用require和import引入模块
- 相对或者绝对路径
- node核心模块
- 当前目录下node_modules
- 父级文件夹查找node_modules
- 系统node全局模块
这样查找不但耗时、而且当前没有引用的包因为其他上级范围存在,那么发布之后会报错。
webpack.base.js
const path = require('path')
module.exports = {
// ...
resolve: {
// ...
modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找
},
}
4.8 tree-shaking css
默认js是开启摇树功能,能去除无用代码。对于css可以这样做:
npm i purgecss-webpack-plugin glob-all -D
webpack.prod.js
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const config = WebpackMerge.merge(BaseConfig, {
mode: 'production',
plugins: [
// 清理无用css
new PurgeCSSPlugin({
// 检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称
// 只打包这些文件中用到的样式
paths: globAll.sync([
`${path.join(__dirname, '../src')}/**/*.tsx`,
path.join(__dirname, '../public/index.html')
]),
})
]
}
4.9 资源懒加载
webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。
import React, { lazy, Suspense, useState } from 'react'
const SomeLazyComponent = lazy(() => import('@/SomeLazyComponent')) // 使用import语法配合react的Lazy动态引入资源
function App() {
const [ show, setShow ] = useState(false)
// 点击事件中动态引入css, 设置show为true
const onClick = () => {
import('./app.css')
setShow(true)
}
return (
<>
<h2 onClick={onClick}>展示</h2>
{/* show为true时加载LazyDemo组件 */}
{ show && <Suspense fallback={null}><SomeLazyComponent /></Suspense> }
</>
)
}
export default App
5. 编码规范
5.1 eslint/prettier
- eslint
npm i eslint -g
通过eslint初始化一些配置
eslint --init
- stylelint
npm install -D stylelint stylelint-order stylelint-config-standard stylelint-order stylelint-config-prettier stylelint-prettier
- prettier npm i prettier -g
npm i prettier -g
.prettierrc.js
module.exports = {
printWidth: 100, // 代码宽度建议不超过100字符
tabWidth: 2, // tab缩进2个空格
semi: true, // 末尾分号
singleQuote: true, // 单引号
jsxSingleQuote: true, // jsx中使用单引号
trailingComma: 'es5', // 尾随逗号
arrowParens: 'avoid', // 箭头函数仅在必要时使用()
htmlWhitespaceSensitivity: 'css', // html空格敏感度
}
然后安装vscode插件。把自动格式化打开。
5.2 husky/commit 规范
安装依赖
npm install --D husky @commitlint/cli
添加git提交的hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
添加配置文件 commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [1, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 72],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
],
],
},
};
验证