前言
新入职公司,某天突然被通知要搭建一个新的中后台项目。慌慌张张、匆匆忙忙的准备下技术选型,虽然 webpack4 已经足够强大,但是 webpack5 已经发布一年多,更新带来很多重大变更,一句话来说就是在更短的时间能打出来体积更小的包。本次分享主要是对在使用 webpack5 构建项目过程中的记录和总结,因为文章篇幅较大,所以分为基础篇:webpack的基本概念、配置和优化篇:对打包时间、打包体积和项目的一些优化,两篇文章来写。本篇为基础篇,优化篇可以查看webpack5实战全解析-优化篇。
本文内容概览
webpack核心概念
无论是 webpack4 还是 webpack5,它都是 webpack 官网,所以在核心概念和配置的写法上没有太大的改变,还是熟悉的味道...
entry(入口)
指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
module.exports = {
entry: './src/main.ts'
}
output(出口)
output 位于对象最顶级键(key),包括了一组选项,指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」。
module.exports = {
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
}
}
loader(加载器)
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。webpack 最出色的功能之一就是,除了引入 JavaScript,还可以通过 loader 或内置的 Asset Modules 引入任何其他类型的文件。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
}
plugins(插件)
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: '项目名称',
}),
]
}
mode(模式)
通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。
module.exports = {
mode: 'development'
}
development
:开发模式,会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 内部处理操作比较少,所以运行和打包比较快。production
:生产模式,会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。内部会启用很多优化操作:FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin,所以打包运行时间比较长。none
:不使用任何默认优化选项
webpack基本配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: '项目名称'
})
]
};
mkdir retail-admin
由于 Webpack 是一个 npm 工具模块,所以我们先初始化一个 package.json 文件,用来管理 npm 依赖版本,完成之后,再来安装 Webpack 的核心模块以及它的 CLI 模块,具体操作如下:
- 一切的开始还要从 mkdir retail-admin 说起,cd retail-admin 执行下面命令,一路回车不回头生成package.json文件
npm init
- 指定 webpack 和 webpack cli 版本安装依赖,防止后面 webpack 版本更新带来不兼容异常,在我搭建项目时,webpack最新版本为5.59.0,webpack-cli最新版本为4.9.1。
npm install webpack@5.59.0 -D
npm install webpack-cli@4.9.1 -D
// package.json
"devDependencies": {
"webpack": "^5.59.0",
"webpack-cli": "^4.9.1"
}
警告
Webpack5 运行于 Node.js v10.13.0+ 的版本,开发环境和打包环境安装的 node.js 版本要高于 v10.13.0。对于前端开发环境的node.js的版本管理可以参考 使用nvm管理node版本,一条龙解决前端开发环境配置
- 在项目根目录新建index.html和源代码目录src,src目录下新建 main.js。webpack4 以后就可以实现零配置打包,我们在 main.js 输入一段代码,然后在终端执行命令
// main.js
let array = [1,2,3,4]
array.map((item)=>{
console.log(item);
})
npx webpack ./src/main.js
如图 webpack 打包成功,报了mode
没有设置的警告。自动生成 dist 目录输出 main.js 文件,输出文件代码转义成 ES6 的箭头函数语法。
-
在项目根目录新建 webpack 配置文件 webpack.config.js
-
添加 webpack 基本配置
// webpack.config.js
const { resolve } = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'main.js',
path: resolve(__dirname, 'dist')
}
}
- 新增 package.json 文件的 scripts 脚本命令
"scripts": {
"build": " webpack --config ./webpack.config.js"
}
- 执行
npm run build
脚本打包成功生成 dist 文件和 main.js 文件
配置babel处理ES6+
Babel 中文官网是 javascript 的编译器,我们日常开发是写 js 代码时用 ES6 语法比较多。但是大部分低版本浏览器并不能兼容这种写法,导致代码无法运行。使用 babel 能在打包时帮我们将不能在旧的浏览器环境中运行的 ES6+ 代码转译成向下兼容版本的 js 代码。
我们在 src/main.js 中写一个箭头函数代码
// src/main.js
[1,2,3,4,5].map((o=>{console.log(o)}));
执行打包命令 npm run build,dist目录下输出的代码依旧包含箭头函数
// dist/main.js
[1,2,3,4,5].map((o=>{console.log(o)}));
- 安装babel依赖
npm install -D babel-loader @babel/core @babel/preset-env
npm install -D @babel/polyfill
npm install -D @babel/plugin-transform-runtime
npm install -D @babel/runtime
npm install -D core-js@3
npm install regenerator-runtime -D
npm install @babel/runtime-corejs3 -D
-
@babel/preset-env
:@babel/preset-env包括支持现代JavaScript(ES6+)的所有插件,主要作用是对我们所使用的并且目标浏览器中缺失的语法和 API 进行代码转换和加载 polyfill。 -
@babel/core
:Babel的核心模块,可以以编程的方式来使用 Babel -
@babel/plugin-transform-runtim
:Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。你可以引入 Babel runtime 作为一个独立模块,来避免重复引入。 -
@babel/runtime
:使用 @babel/plugin-transform-runtim 时将 @babel/runtime 安装为一个依赖
- 配置babel-loader
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: "defaults" }]
]
}
}
}
]
}
在执行打包命令,箭头函数转化为 function
// dist/main.js
[1, 2, 3, 4, 5].map(function (o) {
console.log(o);
});
- 使用了@babel/preset-env后我们可以使用像箭头函数这样 ES6 的新语法,要想使用一些高级API,还要在项目中引入babel垫片。
- 使用了 @babel/polyfill 或者 core-js@3 可以使用新的内置函数(如 Promise 或 WeakMap)、静态方法(如 Array.from 或 Object.assign)、实例方法(如 Array.prototype.includes)和生成器函数。
core-js@3
:@babel/polyfill 无法提供 core-js@2 向 core-js@3 过渡,所以 Babel7.4.0 以上推荐使用新的方案去替代 @babel/polyfill。需要 babel-loader 版本升级到 8.0.0 以上,@babel/core 版本升级到 7.4.0 及以上。
- 修改 babel 配置方式为新建配置文件 .babelrc
preset(预设)
:一组 Babel 插件的集合plugins(插件)
: 通过在配置文件中应用插件(或 预设),可以启用 Babel 的代码转换。
// .babelrc
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
我们在main.js中使用高级API实现一个数组去重,执行打包,在IE9上打开index.html,打开控制台报错。
接下来在项目的入口文件引入垫片文件再进行打包,页面还是报错,这是什么鬼...
// 入口文件main.js
// import "@babel/polyfill" // 卸载掉@babel/polyfill注释引入@babel/polyfill改为使用下面引入
import 'core-js/stable'
import 'regenerator-runtime/runtime'
其实刚才报错是打包后文件内webpack语法的不兼容!这时我们需要新建一个 .browserslistrc 文件,用来配置兼容浏览器范围
// .browserslistrc
> 1%
last 2 versions
not ie <= 8
> 1%
:全球超过1%人使用的浏览器last 2 versions
:所有浏览器兼容到最后两个版本not ie <= 8
:表示IE浏览器版本大于8
在执行打包命令,控制台可以打印出结果
大部分现代浏览器中都可以使用这些高级API,引入垫片主要为了兼容IE浏览器和一些低版本浏览器,如果没有兼容低版本浏览器的需求可以不引入,因为引入后会增加包的体积。此时仅仅写了几行代码的 main.js 打包后体积上升到 584kb,当然这是没有经过优化压缩过的,要不然谁能扛得住。。。
.babelrc和babel.config.js的区别
.babelrc
只会影响本项目中的代码babel.config.js
会影响整个项目中的代码,包含node_modules中的代码
在之前做过的项目里有使用npm安装的第三方插件的包并没有进行打包给到min.js,给的是源代码,这时候在打包时就要使用babel.config.js命名文件对node_modules里面的代码进行babel处理了,否则打包出来的代码包含ES6+语法,无法在低版本的浏览器中运行!!!
HTML文件的创建
html-webpack-plugin 插件会为你的项目生成一个 HTML5 文件, 并且在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。 只需添加该插件到 webpack 配置中就可以实现上面说的功能。
- 安装 html-webpack-plugin 使用文档
npm install -D html-webpack-plugin
- webpack配置
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'main.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'retail-admin',
filename: 'index.html',
template: 'index.html',
inject: true,
scriptLoading: 'defer'
})
]
}
- 主要options说明
title
:用于生成的 HTML 文档的标题。filename
:输入 HTML 内容的文件。template
: 用于生成 HTML 的模板的相对或绝对路径。inject
:设置为 true 时将根据 scriptLoading 选项将 javascript 资源添加到 header 或者 body。scriptLoading
:现代浏览器支持非阻塞 javascript 加载 ('defer') 以提高页面启动性能,设置为 defer 时会在引入 bundle 所生成的 script 标签上增加 defer 属性。minify
:在webpack的mode为production情况下默认为true,会对输出的 HTML 进行压缩。
script标签的defer和async属性
普通script
:文档解析的过程中,如果遇到script脚本,就会停止页面的解析进行下载。
defer
:如果script标签设置了该属性,则浏览器会异步的下载该文件并且不会影响到后续DOM的渲染;如果有多个设置了defer的script标签存在,则会按照顺序执行所有的script;defer脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行。
async
:async的设置,会使得script脚本异步的加载并在加载完毕后执行。async的执行,并不会按着script在页面中的顺序来执行,而是谁先加载完谁执行。
:::
- 在目录下新建一个 index.html 作为模板
我们可以在模板里使用原生语法可以直接写js语句获取 html-webpack-plugin 的参数,还可以进行遍历操作。
- 再执行打包命令,发现在 dist 目录下生成了一个 index.html 文件,在 head 中引入了 我们打包出来的 main.js 引入方式为 defer,title 为我们上面配置的 retail-admin。
- 在浏览器中打开dist/index.html,控制台依次打印出1 2 3 4 5
启用 HMR
我们在前面对webpack进行配置的时候,每次都要进行 build 打包后在刷新浏览器才能看到结果,步骤很繁琐很耗时,好在webpack为我们提供了 webpack-dev-server ,它提供了一个基本的 web server,并且具有实时重新加载功能。设置如下:
- 安装 webpack-dev-server
npm install webpack-dev-server@4.3.0 -D
- webpack 配置
module.exports = {
devServer: {
hot: true,
client: {
overlay: true
},
static: {
directory: pathResolve('public')
},
compress: true,
host: 'localhost',
allowedHosts: ['oms-test.hd123.com'],
port: 9000,
open: true,
proxy: [
{
context: ['/test13', '/v1'],
target: 'https://oms-test-merchant.hd123.com/ui/test13',
changeOrigin: true
}
],
headers: { 'Access-Control-Allow-Origin': '*' }
}
}
hot
:启用 webpack 的热模块替换特性,在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面,能显著加快开发速度。从 webpack-dev-server4开始,HMR 是默认启用的。它会自动应用 webpack.HotModuleReplacementPlugin,这是启用 HMR 所必需的。client.overlay
:当出现编译错误或警告时,在浏览器中显示全屏覆盖static.directory
:在重新编译时,对项目静态文件目录下的文件不进行处理,在使用时直接到对应的静态目录下面去读取文件,这样子就节省了打包运行时花的时间和性能开销。allowedHosts
:该选项允许将允许访问开发服务器的服务列入白名单,这里设置这个参数是因为项目会作为qiankun微前端的子应用接入其他系统,在开发时,项目的资源是在主应用的域名下访问的,所以配置allowedHosts允许主应用的服务访问我们的开发环境。compress
: 为每个静态文件开启gzip压缩headers
: 为所有响应添加 headers,这里配置是为了解决开发环境qiankun微前端跨域的问题host
:指定要使用的 host。如果想让你的服务器可以被外部访问,可以设置为0.0.0.0
,这样也可以通过访问本机ip访问到自己的项目。open
:在服务器已经启动后打开你的默认浏览器。port
: 指定监听请求的端口号proxy
:为开发环境接口请求配置代理服务器,解决开发时访问接口跨域的问题
- proxy代理配置说明
- 对
/test13/user
的请求会将请求代理到https://oms-test-merchant.hd123.com/ui/test13/user
module.exports = {
devServer: {
proxy: {
'/test13': 'https://oms-test-merchant.hd123.com/ui/test13'
}
}
}
- 如果不希望传递/api,则需要重写路径
module.exports = {
devServer: {
proxy: {
'/test13': {
target: 'https://oms-test-merchant.hd123.com/ui/test13',
pathRewrite: { '^/test13': '' }
}
}
}
}
- 如果想将多个特定路径代理到同一目标,则可以使用一个或多个带有 context 属性的对象的数组:
module.exports = {
devServer: {
proxy: [
{
context: ['/test13', '/v1'],
target: 'https://oms-test-merchant.hd123.com/ui/test13',
}
]
}
}
踩坑提醒
使用AJAX封装库axios来发送请求时,baseURL要设置成 ./
否则会出现代理无效的情况!!!
- package.json 新增 npm script,并运行
"scripts": {
"dev": "webpack serve --config ./webpack.config.js"
}
此时 webpack 会帮我们在本地 localhost 的 9000 端口启动一个服务器,部署开发环境打包后的静态资源,运行好后自动打开浏览器展示页面,再在 main.js 中修改内容,直接保存后就会自动在浏览器中更新。
处理样式文件
我们在项目更目录下新建style文件夹,再在文件夹下新建 index.css,写入一段样式代码操作我们index.html的元素。然后在 main.js 中引入,保存后发现报错:
处理.css文件
- 这时就需要安装我们的css-loader,style-loader 配置文档
npm install -D style-loader css-loader
- 配置loader
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
}
]
}
]
}
- 重新执行dev脚本,此时样式已生效,样式代码放在style标签内引入。
处理.scss文件
一般项目里都会用到CSS预处理器,我们可以使用sass去编写样式代码。
- 安装 sass-loader 和 sass 配置文档
npm install sass-loader sass node-sass -D
::: tip 在所有的第三方包中,node-sass 安装起来特别容易异常,下载不下来,我们这边在项目根目录新建 .npmrc 文件,指定 npm 包和 node sass 的下载地址。再进行下载就畅通无阻,杠杠的。 :::
registry=https://registry.npm.taobao.org/
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
- 配置loader
module: {
rules: [
{
test: /\.(sa|sc)ss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
]
}
]
}
使用postcss插件
在处理样式的时候,为了应对不同的浏览器厂商,我们要加上 CSS3 部分属性的浏览器前缀。
- 安装使用 postcss-loader 配置文档,
npm install postcss-loader postcss -D
- 安装自动补齐样式插件 autoprefixer
npm install autoprefixer -D
- 创建 postcss 配置文件 postcss.config.js
module.exports = {
plugins: [require('autoprefixer')]
}
- 配置loader
module: {
rules: [
// 处理.css文件
{
test: /\.css$/,
{
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 1
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
}
]
}
},
// 处理.scss文件
{
test: /\.(sa|sc)ss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
]
}
]
}
- 使用 postcss-loader 也要创建 .browserslistrc 指定浏览器范围兼容,再执行打包,transform前就自动加上了safari、chrome私有属性-webkit-的前缀
集成vue框架
我们这边搭建的项目使用的是vue2的框架,如果想使用其他框架这里可以换成安装其他框架的包和对应的loader、plugin。
- 安装 vue 和 vue-loader Vue Loader官方文档
npm install vue
npm install -D vue-loader vue-template-compiler
vue-template-compiler 需要独立安装的原因是你可以单独指定其版本。每个 vue 包的新版本发布时,一个相应版本的 vue-template-compiler 也会随之发布。编译器的版本必须和基本的 vue 包保持同步,这样 vue-loader 就会生成兼容运行时的代码。这意味着你每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler。
const { VueLoaderPlugin } = require('vue-loader')
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
- Vue Loader v15 现在需要配合一个 webpack 插件才能正确使用:VueLoaderPlugin 这个插件是必须的! 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配
/\.js$/
的规则,那么它会应用到 .vue 文件里的<script>
块。
- 在src目录下新建App.vue文件
// App.vue
<template>
<div id="app">
retail-admin
</div>
</template>
<script lang='ts'></script>
<style scoped lang='scss'>
#app {
width: 100px;
height: 100px;
background-color: #ccc;
}
</style>
- 在入口文件main.js里挂载vue,打开页面上面显示 retail-admin
// main.js
import 'core-js/stable'
import 'regenerator-runtime/runtime'
import Vue from 'vue'
import App from './App.vue'
// import './style/index.css'
// import './style/index.scss'
new Vue({
render: h => h(App)
}).$mount('#app')
asset module
一般在我们的前端项目里,肯定会引入图片或者字体文件这些静态资源,在 webpack5 之前我们处理像图片、字体或者音视频等资源文件时都要通过loader来处理:
raw-loader
:将文件导入为字符串url-loader
:将文件作为 data URI 内联到 bundle 中file-loader
:将文件发送到输出目录
但是在 webpack5 中新增了资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
:发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。asset/inline
:导出一个资源的 data URI。之前通过使用 url-loader 实现。asset/source
:导出资源的源代码。之前通过使用 raw-loader 实现。asset
:在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
处理图片文件
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'static/images/[name].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
}
]
}
处理字体文件
module: {
rules: [
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
type: 'asset',
generator: {
filename: 'static/fonts/[name].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
}
]
}
处理音视频资源
module: {
rules: [
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[name].[ext]'
}
},
]
}
options说明
generator.filename
:自定义输出文件位置以及文件名parser.dataUrlCondition.maxSize
:根据文件大小和maxSize限制大小,自动地在 resource 和 inline 之间进行选择模块类型,maxSize: 10 * 1024,超过10k不导出一个资源的 data URI, data URI的优势是当图片的体积太小,占用一个 HTTP 会话不值得时使用 data URI 可以减少 HTTP 请求,但是 Data URL 形式的图片不会被浏览器缓存所以需要根据项目的实际情况去配置使用。
- 我们在src目录下新建assets/img目录,新增两张图片,一张小于10k,一张大于10k,执行打包命令 npm run build
- 查看在 dist 生成的文件中发现只有一张 cat.jpg 图片,而另一张小图被转化成 base64 编码打到了 main.js 里面。打开 dist 的 index.html,大图是发送请求图片资源,小图是加载base64编码。
使用 svg-icon
项目里面一些纯色图标的使用,UI 同学给到了阿里的 icon-font 字体图标,这里决定用 svg-icon 的形式去使用。最好的方式是使用 svg-sprite-loader 配置文档 来进行处理。对于 svg-icon 具体的使用和组件的封装,可以参考花裤衩大佬的 手摸手,带你优雅的使用 icon,我在项目里面的处理也是参考的这篇文章。
npm install svg-sprite-loader -D
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]'
}
},
]
}
- symbolId:id 属性应该如何命名
注意 使用 asset module 处理静态资源中要将 svg 文件夹排除, 不让 asset module 处理该文件夹
使用Typescript
- 安装依赖
npm install -D typescript ts-loader
- 生成 tsconfig.json
tsc --init
- 安装 ts-loader
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
happyPackMode: true,
appendTsSuffixTo: [/\.vue$/]
}
}
]
}
]
}
- options说明
transpileOnly
: 使用此选项,会关闭类型检查,缩短使用 ts-loader 时的构建时间,搭配fork-ts-checker-webpack-plugin使用实现类型检查happyPackMode
: 使用 HappyPack 或线程加载器来并行化您的构建appendTsSuffixTo
:[/\.vue$/]
,给 vue 文件加上 .ts 拓展名,方便 ts-loader 处理
- 安装 fork-ts-checker-webpack-plugin 使用 fork-ts-checker-webpack-plugin。 它在单独的进程上运行类型检查器,因此构建仍然很快,这是因为在配置 ts-loader 时我们设置了 transpileOnly: true 但仍然可以进行类型检查。 此外,该插件还进行了多项优化以加快增量类型检查。
npm install -D fork-ts-checker-webpack-plugin
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}
- 我们将入口文件名 main.js 改为 main.ts,再把webpack的配置做修改
module.exports = {
entry: pathResolve('src/main.ts')
}
此时编译器报 ts 校验的错,找不到 vue 文件的声明
这是因为在 vue 项目中使用 typescript 我们需要在src目录下新增一个 shims-vue.d.ts 文件,现在就没有报找不到 vue 文件的声明
// shims-vue.d.ts
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
- source map
如果希望能够调试 typescript 源代码,需要对 ts-loader 和 webpack 进行设置:
- 首先修改tsconfig.json配置
{
"compilerOptions": {
"sourceMap": true
}
}
- 其次,需要在 webpack.config.js 中设置 devtool 选项以支持想要的源映射类型。
source map
打包压缩后的代码不是一般的人能看的,如果我们的代码报错了,然而控制台抛出的报错位置是压缩后代码位置,这......;还好有 source map 将编译、打包、压缩后的代码映射回源代码,这就方便我们调试错误了。
- 我们在 App.vue 的第二十行写一行错误的代码,不配置 source map 此时报错信息就不能精准定位到哪一行。
- 现在在 webpack.dev.js 配置 devtool,重新开启热更新,此时报错信息就能精准定位到哪一行了!
devtool: 'eval-cheap-module-source-map'
- source map 一般只在开发环境配置,官方推荐配置为 eval-cheap-module-source-map,其他配置作用和区别可以查看官方文档
优化终端命令行显示
在启动项目和打包项目时,终端没有任何进度内容的展示,我们可以使用 webpack 为我们内置的 ProgressPlugin 插件,在启动项目是可以看到实时的进度和当前运行的loader或者plugin。
打包进度提示
- 使用 ProgressPlugin
const { ProgressPlugin } = require('webpack')
module.exports = {
plugins: [
new ProgressPlugin()
]
}
- 再执行命令行 npm run dev,就能在控制台看到进度信息。
优化打包日志显示
- 在起完项目或者打包完成后会在控制台显示打包出来的资源信息,但一般情况下我们不需要关注这些,只需要知道成功还有失败。
webpack 的 stats 选项让你更精确地控制 bundle 信息该怎么显示
module.exports = {
stats: 'errors-only'
}
errors-only
:只在发生错误时输出信息
- 再执行命令行 npm run dev,控制台输出的信息就节减了很多,但是太过简洁连成功和耗费时间都没有这怎么行?再来...
- 安装使用 friendly-errors-webpack-plugin 使用文档
npm install friendly-errors-webpack-plugin -D
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: ['You application is running here http://localhost:9000'],
notes: ['Some additionnal notes to be displayed unpon successful compilation']
}
})
]
}
- 再执行命令行 npm run dev,控制台输出的信息就完美了,不仅有编译成功和耗时信息,还可以添加项目访问地址和 notes 展示信息。
复制静态文件
对于项目里面的一些不需要经过 webpack 处理的静态资源文件像我们网站的favicon.ico,我们可以直接复制文件夹移动到我们打包后的指定目录下。
- 安装使用 copy-webpack-plugin 配置文档
npm install copy-webpack-plugin -D
const CopyWebpackPlugin = require("copy-webpack-plugin")
module.exports = {
plugins: [
// 复制静态文件
new CopyWebpackPlugin({
patterns: [
{
from: resolve(__dirname, 'public'),
to: 'static/public',
toType: 'dir'
}
]
})
]
}
- 上面的配置是将项目下的public目录直接拷贝到打包输出的文件夹下面的static目录
-
在项目目录下新建 public 目录,放入文件 favicon.ico
-
执行打包命令 npm run build,在生成的dist/static目录下新增了public 文件夹。
注入全局环境变量
webpack 内置的 DefinePlugin 插件可以在编译时将你代码中的变量替换为其他值或表达式,我们可以用来实现注入全局环境变量
- 配置DefinePlugin
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
})
]
}
- 在入口文件 main.js 中打印 NODE_ENV 和 process.env.NODE_ENV
import 'core-js/stable'
import 'regenerator-runtime/runtime'
import Vue from 'vue'
import App from './App.vue'
// import './style/index.css'
// import './style/index.scss'
console.log(NODE_ENV)
console.log(process.env.NODE_ENV)
new Vue({
render: h => h(App)
}).$mount('#app')
- 发现我们注入的 NODE_ENV 竟然是 undefined,process.env.NODE_ENV 为 development,不是将 mode 设置为 development 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development 吗?是的,在webapck执行完后确实设置了。所以这里我们要使用 cross-env 来帮我们设置环境变量,cross-env 是运行跨平台设置和使用环境变量的脚本。
- 安装 cross-env
npm install cross-env -D
- 修改 npm script
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config ./webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js"
}
- 再执行脚本命令 npm run dev,这时候就能正常打印了
文件指纹
我们在打包后 webpack 会根据 chunk 依赖生成 bundle,然后输出成文件。每次前端发布并不是所有的文件都有代码变更。这时候我们可以利用浏览器缓存,客户端加载时只请求有变更的文件,没有变更的文件走缓存。在实现上我们可以对于css、js、图片文件使用强缓存然后给我们打包出来的文件加上hash值,从哈希值的变更上来判断文件的更新。webpack 给我们提供了三种哈希值计算方式,分别是hash、chunkhash和contenthash:
-
hsah
:和整个项目的构建有关,只要项目文件有修改,整个项目构建的hash值就会更改。 -
chunkhash
:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值。 -
contentHash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变。
根据三种 hash 的用法,我们一般对出口 js 文件设置 chunkhash;一般在项目中的 css 都抽离出对应的 css 文件来加以引用。如果我们使用 chunkhash,当我们改了css 代码之后,会发现 css 文件 hash 值改变的同时,js 文件的 hash 值也会改变,所以 css 文件设置 contenthash;图片或者字体文件使用 hash 值。我们在配置中加上 hash 值:
output: {
// filename: 'main.js',
filename: 'static/js/[name].[chunkhash:8].js',
path: pathResolve('dist')
}
{
test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
type: 'asset',
generator: {
// filename: static/images/[name].[ext]
filename: 'static/images/[name].[hash:8].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
}
再执行打包命令,发现文件名上拼接上了一串哈希值,此时不对文件做修改再打包发现哈希值没有变化。
现在我们对 main.js 入口文件稍作修改,重新执行打包命令,此时 main.js 的哈希值就已发生了改变。
清除dist目录
我们配置了文件指纹后每次改变文件再打包,哈希值都会变,次数多了我们的 dist 文件夹显得相当杂乱,而且之前的文件现在都已经没用了,所以比较推荐的做法是,在每次构建前清理 dist 文件夹,这样只会生成用到的文件。
让我们使用 output.clean 配置项实现这个需求,配置后会在生成文件之前清空 output 目录。这也是 webpack 5.20.0+ 新增的配置,之前的版本都要安装插件去实现这个功能,现在webpack5帮我们内置了这个功能只要配置下就行了:
module.exports = {
output: {
clean: true
}
};
构建配置包设计
在我们实际的前端项目中一般都是分为开发环境和生产环境,开发环境用于我们编写代码调试功能,而生产环境就是打包部署我们写好的代码发布到线上服务器。对于不同的环境要求 webpack 所提供的功能也不一样,所以这里我们要对配置文件进行区分。
webpack.common.js
:开发环境和生产环境公用的配置
我们在项目目录下新建webpack文件夹,用来放置 webpack 打包配置文件,在目录下新建 webpack.common.js 文件,将公用的配置从之前的 webpack.config.js 中挪过去
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { ProgressPlugin } = require('webpack')
const CopyWebpackPlugin = require("copy-webpack-plugin")
const webpack = require('webpack')
module.exports = {
entry: './src/main.js',
output: {
filename: 'static/js/[name].[chunkhash:8].js',
path: resolve(__dirname, 'dist'),
clean: true
},
stats: 'errors-only',
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
happyPackMode: true,
appendTsSuffixTo: [/\.vue$/]
}
}
]
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /\.(sa|sc)ss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /\.(png|jpe?g|gif|webp|svg)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'static/images/[name].[hash:8].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
type: 'asset',
generator: {
filename: 'static/fonts/[name].[hash:8].[ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[name].[hash:8].[ext]'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]'
}
},
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'retail-admin',
filename: 'index.html',
template: 'index.html',
inject: true,
scriptLoading: 'defer'
}),
new ForkTsCheckerWebpackPlugin(),
// 请确保引入这个插件!
new VueLoaderPlugin(),
new ProgressPlugin(),
// 复制静态文件
new CopyWebpackPlugin({
patterns: [
{
from: resolve(__dirname, 'public'),
to: 'static/public',
toType: 'dir'
}
]
}),
new webpack.DefinePlugin({
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
})
]
}
webpack.dev.js
:开发环境 webpack 配置
新建 webpack.dev.js 文件,将开发环境的配置从之前的 webpack.config.js 中挪过去,开发环境的 mode 设置为 development。此时我们还需要用到 webpack 提供的 merge 函数,对公用的配置和开发的配置进行合并。
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const { resolve } = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
hot: true,
client: {
overlay: true
},
static: {
directory: resolve(__dirname, 'public')
},
compress: true,
host: 'localhost',
allowedHosts: ['oms-test.hd123.com'],
port: 9000,
open: true,
proxy: [
{
context: ['/test13', '/v1'],
target: 'https://oms-test-merchant.hd123.com/ui/test13',
changeOrigin: true
}
],
headers: { 'Access-Control-Allow-Origin': '*' }
},
plugins: [
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: ['You application is running here http://localhost:9000'],
notes: ['Some additionnal notes to be displayed unpon successful compilation']
}
})
]
})
webpack.prod.js
:生产环境 webpack 配置
新建 webpack.dev.js 文件,将生产环境的配置从之前的 webpack.config.js 中挪过去,生产环境的mode设置为 production
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production'
})
- 修改 npm script 后执行开发命令和生产打包命令
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config ./webpack/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./webpack/webpack.prod.js"
}
启用热更新后发现在控制台报了一个错,无法定位到 'E:\learnCode\retail-admin\webpack\public' 这个文件夹
执行打包命令后发现 dist 目录被放到了 webpack 目录下面...陷入沉思...
出现上面的原因是因为我们在配置的时候指定文件路径用的是 resolve(__dirname, 'public')
这个路径指向的是执行的配置文件同级目录下面public文件夹,这当然会报错了,因为压根就没有这个文件夹,应该要指向项目根目录下面的public文件夹才正确。
这是我们更改文件路径的指向,新增一个 pathResolve 函数
function pathResolve(dir) {
return resolve(process.cwd(), '.', dir)
}
将使用 resolve(__dirname, '') 的地方都替换成 pathResolve()
// entry: './src/main.js',
entry: pathResolve('src/main.js')
// path: resolve(__dirname, 'dist')
path: pathResolve('dist')
new CopyWebpackPlugin({
patterns: [
{
// from: resolve(__dirname, 'public'),
from: pathResolve('public'),
to: 'static/public',
toType: 'dir'
}
]
})
修改完后再执行脚本命令,就发现 no problem 了
tip 路径指向说明
path.resolve()
: 把一个路径或路径片段的序列解析为一个绝对路径。__dirname
: 总是指向被执行 js 文件的绝对路径process.cwd()
:当前Node.js进程执行时的工作目录
完结
至此我们已经实现了一个简单的 webpack 打包配置,可以进行本地开发和产出生产前端资源包。但是这还远远不满足我们发布生产上线的条件,可以优化的空间还很大。
参考文献:
往期文章: