前言
目前公司前端体系还不太完善的原因,一般写项目用的都是使用 create-react-app 搭建的一套通用的脚手架。这个脚手架虽然非常的通用、基础,但是在搭配了 tailwindcss、postcss 后,每次的编译和打包消耗的时间实在是太久了,非常影响工作效率。那么,为了不加班,手动来搭建一套通用的 webpack
脚手架,就成为了当前的一个目标。
正文
项目基本配置
项目初始化
mkdir webpack-demo-1
cd ./webpack-demo-1
yarn init -y
首先需要创建项目的入口文件和 webpack
的配置文件,此时项目目录如下:
然后,需要安装 webpack 依赖
yarn add --dev webpack webpack-cli
这里用到的 webpack
相关版本如下:
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2"
写入内容
在 index.js
中随便写点东西进去:
class Hello {
constructor() {
console.log('hello webpack!')
}
}
const test = new Hello()
然后,我们运行 webpack
来体验一下具体是什么效果
npx webpack
可以看到,在运行完以后,在根目录下会增加一个 dist 目录,该目录下会新增一个 main.js 文件
其中的内容:
new class{constructor(){console.log('hello webpack!')}};
那这样肯定是不 OK 的呀,需要使用 babel 把 ES6 转换成 ES5 的代码。下面就来安装一些 babel 的一些相关依赖。
配置 babel
安装依赖:
yarn add --dev babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties @babel/plugin-proposal-private-methods
yarn add @babel/runtime @babel/runtime-corejs3
修改 webpack.config.js
文件:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash:8].js',
},
module: {
rules: [
{
test: /\.(jsx|js)$/,
use: 'babel-loader',
exclude: /node_modules/,
},
]
}
}
在根目录下创建 .babelrc
文件:
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-transform-runtime", {"corejs": 3}],
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }]
]
}
让我们再执行一次 npx webpack
来看看输出结果。此时的项目目录:
来查看一下这个 dist 目录下的 bundle.xxxx.js
文件的内容:
(()=>{"use strict";new function n(){!function(n,o){if(!(n instanceof o))throw new TypeError("Cannot call a class as a function")}(this,n),console.log("hello webpack!")}})();
这样应该就没什么问题了。接下来来让项目运行在浏览器中吧!
运行在浏览器中
这里直接使用 webpack 生态圈里一个非常知名的插件 html-webpack-plugin
,这个插件可以让我们的构建产物使用我们指定的 html
文件作为模板来使用。
yarn add --dev html-webpack-plugin
在根目录下,创建一个 public 文件夹,并放入一个 index.html
文件,并给他写入最基本的 html 的内容
在 wbepack.config.js
文件中使用 html-webpack-plugin
插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html'),
inject: 'body',
scriptLoading: 'blocking',
}),
]
}
此时,在运行 npx webpack
查看打包结果
打开 dist
文件夹目录,直接用浏览器打开这个 index.html
文件
此时可以看到我们输入的 "hello webpack!" 已经显示在控制台中了。
目前存在的问题
以上就算是跑通了一个最基本的流程,但当前还面临几个最大的问题:
- 需要热更新。不可能每次更新内容后都重新 build 一次,去使用打包后的文件来做调试;
- 在每次打包前需要清除上一次打包的内容。
- 环境拆分 那么接下来,先来解决这两个问题吧!
热更新
这里我们根据官网提示,使用 webpack-dev-server
yarn add --dev webpack-dev-server
然后需要在 webpack.config.js
中添加上相关的配置内容:
module.exports = {
// ...
devServer: {
port: '8080', // 开启的端口号,一般是 8080
hot: true, // 是否启用 webpack 的 Hot Module Replacement 功能,也就是模块热替换
stats: 'errors-only', // 终端仅打印 error
compress: true, // 是否启用 gzip 压缩
},
}
之后我们需要在 package.json
中添加一个 ``scripts 命令
{
// ...
"scripts": {
"start": "webpack serve --open"
}
}
此时,就可以使用 yarn start
命令,在浏览器中打开 http://localhost:8080/
页面,并可以使用热更新来进行调试,真是太方便了!
清除旧的打包产物
这个问题很好解决,直接使用 clean-webpack-plugin
插件即可。但是,要注意的是,在 webpack
5.20 版本后,webpack
的 output
已经支持在每次打包前清除构建产物,仅需在 webpack.config.js 中,在 output 字段中添加 clean: true
即可实现与 clean-webpack-plugin
相同的效果
// ...
module.exports = {
...
output: {
// ...
clean: true
}
}
环境拆分
一般来说,一个项目会分为 开发
、预发
和生产
环境,这里的话主要还是分为开发
和生产环境
更新目录结构,在根目录下创建 build
文件夹,将原来根目录下的 wbepack.config.js
删除,在 build
目录下创建 webpack.base.config.js
(公共部分)、webpack.dev.config.js
(开发部分)、webpack.prod.config.js
(生产部分) 三个文件。
webpack.base.config.js
:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const rootDir = process.cwd();
module.exports = {
entry: path.resolve(rootDir, 'src/index.js'),
output: {
path: path.resolve(rootDir, 'dist'),
filename: 'bundle.[contenthash:8].js',
},
module: {
rules: [
{
test: /\.(jsx|js)$/,
use: 'babel-loader',
include: path.resolve(rootDir, 'src'),
exclude: /node_modules/,
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(rootDir, 'public/index.html'),
inject: 'body',
scriptLoading: 'blocking',
}),
],
}
在配置生产和开发环境时,需要安装一个 webpack-merge
的插件,用于合并配置。
yarn add --dev webpack-merge
webpack.dev.config.js
:
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
port: '8080', // 默认是 8080
hot: true,
stats: 'errors-only', // 终端仅打印 error
compress: true, // 是否启用 gzip 压缩
},
});
webpack.prod.config,js
:
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
module.exports = merge(baseConfig, {
mode: 'production',
});
最后,要注意,更新一下 package.json
文件中的 scripts
命令:
"scripts": {
"start": "webpack serve --config build/webpack.dev.config.js --open",
"build": "npx webpack --config build/webpack.prod.config.js"
}
继续完善功能
支持 sass 和 css
首先,在 src 目录下添加一个 index.scss
文件,并在 index.js
文件中引入,此时在执行 yarn start
后,会发现,webpack
是无法在没有安装对应 loader 的情况下,识别 scss 和 css 文件的内容。
接下来让我们来安装 loader。
yarn add --dev sass dart-sass sass-loader css-loader style-loader
注意:安装 sass
一般需要配合 node-sass
来使用,但根据官方文档,会发现其实他们更推荐使用的是 dart-sass
,如果使用过 creat-react-app
来配置 sass
的同学会发现,create-react-app
是不支持 dart-sass
的。不过在这里我们自己手动配置,那使用 dart-sass
自然是没有问题的。
然后,修改 webpack.base.config.js
文件
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(s[ac]ss|css)$/i,
exclude: /node_modules/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
]
},
// ...
}
此时,再使用 yarn start
运行项目,就可以看到 css 在项目中已经可以展示啦!
添加 postcss
接下来,我们来加入 postcss
yarn add --dev autoprefixer postcss postcss-loader
更新 webpack.base.config.js
文件
const autoprefixer = require('autoprefixer');
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(s[ac]ss|css)$/i,
exclude: /node_modules/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: "postcss-loader",
options: {
postcssOption: {
plugins: [
["autoprefixer"]
]
}
}
}
]
},
]
},
// ...
}
打包后抽离 css 文件
首先安装 mini-css-extract-plugin
插件
yarn add --dev mini-css-extract-plugin
更新 webpack.base.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(s[ac]ss|css)$/i,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
["autoprefixer"]
]
}
}
}
]
},
]
},
plugins: [
// 省略...
new MiniCssExtractPlugin({
filename: 'css/[name].css',
}),
],
}
此时,再运行 yarn build
来查看一下
可以看到,css 文件已经被抽离到指定的目录下了。
把静态资源复制到打包目录
有时候可能会有一份静态的、需要手动下载下来的文件,需要手动加入到项目中。通常情况下,需要把这份资源放入到我们的 public
文件目录中,然后在 index.html
中用 script
导入。
但实际情况是,在 public
目录下添加后,还是无法找到这个文件。
// index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./js/test.js"></script>
</body>
</html>
编译结果:
那么这时候就需要用到 copy-webpack-plugin
这个插件,在打包构建时,把指定的文件复制到打包的产物中。
yarn add --dev copy-webpack-plugin
更新 webpack.base.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
const rootDir = process.cwd();
module.exports = {
// ...
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: '*.js',
context: path.resolve(rootDir, "public/js"),
to: path.resolve(rootDir, 'dist/js'),
},
],
})
new MiniCssExtractPlugin({
filename: 'css/[name].css',
}),
new OptimizeCssPlugin(),
],
}
再次运行 yarn start
现在这个静态的 js 文件已经可以成功加载了
加载图片资源
前端项目,自然免不了要引入图片等资源,此时,我们尝试在项目中引入资源。 在 index.js 中引入图片:
import './index.scss'
import imgTets from './assets/1.png'
class Hello {
constructor() {
console.log('hello webpack!')
}
renderImg() {
const img = document.createElement('img')
img.src = imgTets
document.body.appendChild(img)
}
}
const test = new Hello()
test.renderImg()
运行项目,会看到一个熟悉的报错:
正常来说,那就按提示走,缺少什么 loader
,那就直接 yarn add xxx-loader
就完事了,那这里就安装 raw-loader
、url-loader
、file-loader
,一把梭就行了。
但是,要注意的是,在 webpack 5
中,并不需要再安装这些依赖了,只需在 webpack.base.config.js
的配置中加上:
rules: [
// ...
{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
type: 'asset',
},
]
重新运行一遍 yarn start
,此时再运行,就没有问题啦!
构建阶段的项目优化
缓存
webpack 5 已经为我们做了许多事情,其中就包括了缓存。配置:
// webpack.dev.config.js
module.exports = merge(baseConfig, {
mode: 'development',
//...
cache: {
type: 'memory'
},
});
// webpack.prod.config.js
module.exports = merge(baseConfig, {
mode: 'production',
// ...
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
},
});
然后我们来尝试运行两次 yarn build
,来查看前后的时间差
可以看到前后的差距,还是比较大的
代码拆分
分割各个模块代码,提取相同部分代码,好处是减少重复代码的出现频率。从 webpack 4 开始,就开始使用 splitChunks
来代替 CommonsChunksPlugin
做代码的拆分操作。
配置:
// webpack.base.config.js
const webpack = require('webpack');
module.exports = {
//...
plugins: [
new webpack.optimize.SplitChunksPlugin(),
],
optimization: {
splitChunks: {
chunks: 'all' // 代码分割类型:all全部模块,async异步模块,initial入口模块
}
},
}
多线程打包
项目的打包速度在大型项目里是一个比较令人头疼的点,这里我们利用 thread-loader
来对项目进行多线程打包。
安装依赖:
yarn add --dev thread-loader
更新 webpack.base.config.js
module.exports = {
entry: path.resolve(rootDir, 'src/index.js'),
output: {
path: path.resolve(rootDir, 'dist'),
filename: 'bundle.[contenthash:8].js',
clean: true
},
module: {
rules: [
{
test: /\.(jsx|js)$/,
use: ['thread-loader', 'babel-loader'],
include: path.resolve(rootDir, 'src'),
exclude: /node_modules/,
},
{
test: /\.(s[ac]ss|css)$/i,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
'thread-loader',
'css-loader',
'sass-loader',
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
["autoprefixer"]
]
}
}
}
]
},
{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
type: 'asset',
},
]
},
//...
}
阶段一总结
到这里为止,就是一个比较通用的、基于 webpack 5
的脚手架了。但是,要直接拿来开发,还是需要再做一些操作,因为,连 react
或者 vue
都还没有安装配置呢!
在阶段二,我需要在这个脚手架的基础上,根据自己的需求,添加 react
、typescript
、tailwindcss
等功能,以满足日常开发的需要!