Skip to main content

4 posts tagged with "前端工程化"

View All Tags

· 8 min read

这阵子把组内最大的前端项目做了下 webpack 的升级,构建效率直线上升,主要得益于 webpack5 的缓存策略

webpack5 带来了什么?

  • 持久化缓存。webpack4 需要通过 cache-loader/hardSourcePlugin 来实现中间缓存,webpack5 相当于内置了这部分功能
  • 更好的 hash 算法。hash-->fullhash,比如 webpack 4 如果添加空白、注释或修改变量名是会影响 contenthash 值的计算,webpack5 则不会影响,从而能继续使用缓存,这个方式降低了缓存的失效率,间接加快了应用 rebuild 的速度
  • Asset Modules。指的是图片和字体等这一类型文件模块,它们无须使用额外的预处理插件
  • 模块联邦。实现应用级的模块复用,这个是现在蛮多新兴微前端框架的基础
  • tree-shaking 改进。据说可以减少约 30%的 bundle-size,不过实际项目中并没有体会到这个变化
  • 更严格的代码检查。这个也是造成很多 webpack4 项目在升级后突然在运行时报错的原因,会导致页面 crash
  • 确定的 moduleId/chunkId
  • Node Polyfill 脚本被移除
  • 原生 worker 支持。可以参考 react 项目中使用 web worker 里面有提及 webpack5 下如何使用 web worker

开始升级

这个过程也是遇到了一些问题,总结如下:

因为我们的项目是基于 CRA4 搭建的,并且基于 react-app-rewired 扩展 webpack 配置,所以首先需要升级 react-scripts、react-app-rewired、customize-cra

额外引入的 loader、plugin 都要升级到支持 webpack5 的版本。没有支持 webpack5,那就去 github issue 里面看看啥时候支持?

首先是 addLessLoader 不适配,需要更换插件如下:

// config-overrides.js
const addLessLoader = require("customize-cra-less-loader");
//...
addLessLoader({
lessOptions: {
javascriptEnabled: true,
sourceMap: false,
},
}),

可以借助 npm-check-updates 这个插件检查所有依赖版本

去除一些废弃的插件,比如以下的资源插件

  • url-loader 将文件作为 dataURI 内联到 bundle 中
  • file-loader 将文件发送到输出目录
  • raw-loader 将文件导入为字符串

webpack5 通过以下配置就可以完成对资源文件的解析

  • asset/resource 发送一个单独的文件并导出 URL(file-loader)
  • asset/inline 导出一个资源的 dataURI(url-loader)
  • asset 在导出一个 dataURI 和发送一个单独的文件之间自动选择。webpack4 需要通过使用 url-loader 并且配置资源体积限制来实现
  • asset/source 导出资源的源代码

配置方式参考:

// ...
rules: [
{
test: /\.png$/i,
use: 'file-loader'
},
{
test: /\.(jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
},
},
],
}
],
// 改成
// ...
rules: [
{
test: /\.png$/i,
use: 'asset/resource'
},
{
test: /\.(jpg|gif)$/i,
type: 'asset',
parser: {
dataurlCondition: {
maxSize: 1024 // 单位是B
}
}
}
],

这里可以参考下我给项目做的资源配置

// config-overrides.js
// ...
addWebpackModuleRule({
test: /\.(png|jpe?g|gif|webp|svg|bmp|ttf|eot|woff|woff2)$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 1024 * 1024,
},
},
generator: {
filename: "img/[name].[hash:4][ext]",
publicPath: process.env.PUBLIC_URL,
},
}),
addWebpackModuleRule({
test: /\.(objs?|mtl)$/i,
type: "asset/resource",
generator: {
filename: "model/[name].[hash:4][ext]",
publicPath: process.env.PUBLIC_URL,
},
}),
// ...

去除 hard-source-webpack-plugin,取而代之的是 cache

cache: {
// memory(内存)|filesystem(持久化缓存)
type: "filesystem",
buildDependencies: {
config: [__filename],
},
version: '1.0'
}

缓存文件会保存在 node_modules/.cache。参考 https://github.com/webpack/webpack/issues/6527

如果配置 filesystem 做持久化储存,webpack5 还是会同时使用 memory,用于 watch 模式

如果是 eject 导出完整 webpack 配置的项目,可能还会遇到以下问题:

部分插件的引入方式需要调整,比如 webpack-merge 改成解构的方式引入

const webpackMerge = require("webpack-merge");
const ManifestPlugin = require("webpack-manifest-plugin");
// 改成
const { merge: WebpackMerge } = require("webpack-merge");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");

如果有单独引用 dev-sever 也要升级,启动命令调整如下:

// package.json
{
"scripts": {
"serve": "webpack-dev-server --config webpack.config.dev.js"
}
}
// 改成
{
"scripts": {
"serve": "webpack server --config webpack.config.dev.js"
}
}

通过 require 引入的图片会无法正常加载,需要在 url-loader 配置中添加 esModule: false

sourcemap 的名称可能也需要调整

devtool: "cheap-eval-module-source-map";
// 改成
devtool: "eval-cheap-module-source-map";

开启 hot: true 热更新无效,需要添加配置如下:

{
target: process.env.NODE_ENV === "development" ? "web" : "browserslist",
}

这像是一个 bug,参考 https://github.com/webpack/webpack-dev-server/issues/2758

除去编译问题后,接下来主要在于解决一些运行时的报错,基本上按照提示一个个去解就行了

JSON 模块只能使用默认引入,调整如下

import { name } from "package.json";
// 改为
import pkgInfo from "package.json";
const { name } = pkgInfo;

其他的问题比如重复的函数或变量、undefined 等,在 webpack5 更严格的检查下会暴露,逐个修复就行了

进一步优化

CRA5 在 webpeck 的配置上已经做了很充分的优化工作,参考源码 https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/config/webpack.config.js

进一步优化的空间其实不多,可以做下 code spilting,配置参考:

// config-overrides.js
module.exports = {
webpack: override(
// ...
isProduction &&
setWebpackOptimizationSplitChunks({
chunks: "all",
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)/,
name: "react",
priority: 1,
},
three: {
test: /[\\/]node_modules[\\/](three)/,
name: "three",
priority: 1,
},
antd: {
test: /[\\/]node_modules[\\/](antd|@ant-design)/,
name: "antd",
priority: 1,
},
echarts: {
test: /[\\/]node_modules[\\/](echarts|zrender)/,
name: "echarts",
priority: -1,
},
antv: {
test: /[\\/]node_modules[\\/](@antv)/,
name: "antv",
priority: -1,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
priority: -5,
},
},
})
),
};
// ...

如果是多页或者多路由页面的应用,还可以结合 React.lazy 进行页面级别的分包,按需加载页面及其资源

总结

实际升级下来,其实问题不多,花个一天左右就填完坑了,可能是 react-scripts@5.0 已经做了大部分工作了,然后疑难问题基本上都可以找到解决方法。升级效果也很明显,主要体现在 rebuild 的速度上。我觉得还没升级的话,可以大胆升级一波,升级完之后就可以尝试 webpack5 的各种新特性啦 ~

· 8 min read

前端工程化其实是软件工程在前端方面的应用,是指将系统化的、规范化的、可度量的方法用于前端应用的开发、运行和维护的过程。其主要的作用有:

  • 提升生产效率
  • 降低前端开发成本,解放生产力
  • 提高前端应用质量(质量保证)
  • 降低企业成本

简单说就是研究如何 降本提效和更好地支撑业务

前端工程化的思维导图参考下图: image.png

一个简单的研发流程如下: image.png

以上其实只是我个人现阶段对前端工程化的理解,还会持续更新。文章也主要基于以上这个流程来阐述下前端工程化

脚手架

大厂基本上都会自研脚手架,通过 node-cli 去选择和拉取需要的脚手架到本地进行开发,除了统一的项目结构,此时脚手架里面还包含了这次项目所需的依赖和一些基础配置文件,包括 eslintprettier等代码规范的配置文件

开发

这个阶段是对编码规范、git 规范、项目规范的实践

编码规范

可以参考 Google 的代码风格指南

Javascript 风格指南

TypeScript 风格指南

Git 规范

分支管理规范

  • master、release、develop、feature
  • 基于 develop 创建新的 feature 分支进行开发
  • 如果是多人协作,则需要先额外约定一个合并分支,再基于该分支创建新的 feature 分支

commit 规范

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
  • type
feat: 新功能、新特性
fix: 修改 bug
perf: 更改代码,以提高性能
refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改)
docs: 文档修改
style: 代码格式修改, 注意不是 css 修改(例如分号修改)
test: 测试用例新增、修改
build: 影响项目构建或依赖项修改
revert: 恢复上一次提交
ci: 持续集成相关文件修改
chore: 其他修改(不在上述类型中的修改)
release: 发布新版本
workflow: 工作流相关文件修改
  • scope: 这次 commit 影响的范围, 可以是文件夹或者某个文件
  • subject: commit 的概述
  • body: commit 具体修改内容, 可以分为多行
  • footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接

借助 husky 验证 commit 规范,主要通过 git 的 pre-commit 钩子函数来进行

npm i husky -D

然后在你项目根目录下新建一个文件夹 script,并在下面新建一个文件 commit-check.js,输入以下代码:

const msgPath = process.env.HUSKY_GIT_PARAMS;
const msg = require("fs").readFileSync(msgPath, "utf-8").trim();

const commitRE =
/^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/;

if (!commitRE.test(msg)) {
console.error(`commit 消息不规范`);
process.exit(1);
}

最后在 package.json 加上下面的代码

"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "node script/commit-check.js",
"pre-push": "npm test"
}
}
  1. commit 之前检查代码格式
  2. 检查 commit message
  3. 将代码推送到远程仓库前,执行 npm test 进行测试,测试失败则不会推送

项目规范

项目结构

文件名称统一使用小写,名称过长用 - 隔开

├─assets (静态资源)
├─components (公共组件)
├─containers (页面)
├─styles (公共样式)
├─routes (路由)
├─store (数据管理)
├─services (接口函数)
├─helper (工具文件夹)
├─request (请求函数)
└─utils (工具函数)

其他

  • 权限配置
  • 路由配置
  • 菜单
  • ...

ui 规范

ui 规范需要由前端、UI/UE、产品商量和制定,建议使用统一的组件库

统一 ui 标准,可以减少前端在开发中由于 ui 设计额外增加的工作量

测试

前端其实很少会做比较规范的测试,毕竟这事吃力不讨好。比较多的应该是用 Jest 做单元测试,主要针对工具函数和公共组件,确保这部分高复用代码的质量

构建

构建这一步主要看使用的构建工具吧,现在主流的还是 webpack,所以针对 webpack 构建会创建好最基础的配置,然后可以做一些编译优化来提升构建速度。以及做持续构建和集成,这一步具体做啥我还不清楚

部署

简单的持续部署可以利用 jenkinswebhook 钩子函数来实现,webhook 会监听某个事件,当触发某个事件(比如 push 事件)时,通过钩子函数通知 jenkins 并执行预先设置好的脚本去部署应用

部署时也要讲究一定的策略,减少上线过程造成的异常。可以参考 大公司里怎样开发和部署前端代码?,里面有讲到一些蛮不错的方法

监控

主要分为 性能监控异常监控。作用是监视应用情况、预警和定位问题,然后可以根据应用情况来做一些有效的性能优化。可以通过买 sentry 的服务或者自研的方式实现一个监控系统

性能监控

一般借助 window.performance 来采集网页的性能指标

image.png

初探 performance – 监控网页与程序性能

如何进行 web 性能监控

异常监控

异常监控其实就是收集应用的报错信息,主要收集以下三种报错信息:

  • 资源加载错误
  • js 执行报错
  • 异步错误,比如 promise。

通过报错收集,可以了解到网站发生错误的类型和数量,从而可以做相应的预防措施来减少网站异常的问题

沉淀了 3 年的自研前端错误监控系统,打通你的脉络

埋点

  • 性能数据上报,由性能监控消费
  • 异常数据上报,由异常监控消费
  • 用户数据收集和分析(navigator、UV、PV、跳转来源、页面停留时间...)
  • 业务数据收集...

最后

以上只是我现阶段对前端工程化的理解和概括,后面我会对工程化的各个环节做更深的探索和实践,进一步夯实前端工程化能力

· 11 min read

本文主要是记录 Git 在实际开发中的常见用法和技巧

环境配置(Mac)

1. 检查环境变量

在终端输入 git,如果出现下面的提示,则需要先初始化 xcode

Agreeing to the Xcode/iOS license requires admin privileges, please run “sudo xcodebuild -license” and then retry this command.

打开 Xcode 软件进行初始化即可,之后再次输入'git',出现下面的提示则说明,环境变量配置成功

usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
<command> [<args>]

2. 配置用户基本信息

每一条提交都会产生一条记录,这条记录标记了提交人的姓名与邮箱,以便他人查看与联系。

git config --global user.name "JacksonZhou"
git config --global user.email "1359926897@qq.com"
// 查看全局配置
git config --list

3. 配置 ssh

配置了 SSH 到指定服务器上,即说明让服务器信任你的笔记本,每次从 gitlab 或 github 拉代码和上传代码时不用输入用户名和密码

生成 ssh key

ssh-keygen -t rsa -C "1359926897@qq.com"

检查是否生成成功

cat ~/.ssh/id_rsa.pub
// ssh key 生成成功的话,就会看到如下输出
// ssh-rsa ......

复制 ssh-rsa 后面的内容,粘贴到你服务器网站的 SSH Key 配置项里面即完成配置,接下来就可以拉取代码了。

Git 基本操作

先看下 Git 的基本工作流 image.png 下面是一些实际开发过程常见的操作,更详细的内容建议查看 Git 中文手册

工作区 <--> 远程仓库

克隆远程仓库到本地

  • git clone
git clone origin <remote_repo>
git clone -b <branch> <remote_repo> // 从指定分支拉取代码

连接远程仓库

  • git remote
git remote add origin <remote_repo>     // 链接远程仓库
git remote set-url origin <remote_repo> // 修改远程仓库
git remote rm origin // 删除远程仓库
git remote -v // 查看远程仓库列表
git remote show origin // 查看具体仓库细节

upstream

在 Github 上,我们可以 fork 任意开源项目到个人 ID 下,当我们把个人 ID 下的仓库 clone 到本地时,除了默认的 origin 仓库外,我们还需要配置一个 upstream 仓库,指向原始的开源项目地址,保证可以定期同步源仓库的更新。(用的比较少)

git remote add upstream <git_repository_url> // 建立 upstream 仓库
git remote remove upstream // 删除远程 upstream 仓库

实际开发过程中,如果没有先确定分支的追踪关系,当我们使用 git pull 或 git push 时就需要指定从远程的哪个分支拉取合并和推送到远程的哪个分支。

git branch -u <> <>    // 指定本地分支和远程分支的追踪关系
git push -u <> <> // 确定追踪关系并提交代码

与远程仓库同步

  • git pull
git pull origin <branch> // git fetch + git merge,从指定分支拉取代码并更新
git pull --rebase // git fetch + git rebase,不会生成 merge 记录,建议是用于个人分支

工作区 <--> 暂存区

提交

  • git add
git add .
git add -i // 筛选想要提交的修改并提交

撤销

  • git restore

暂存区 <--> 本地仓库

提交

  • git commit
git commit -m "" // 提交代码到本地仓库

重新提交

如果发现已经提交了改动,但是还有修改也是需要合到那一部分改动中的,可以这样操作

git add .
git commit --amend -m ""
// amend 也可以单纯用来修改 commit 信息

撤销

  • git reset - 适用于本地记录,即还没提交到远程仓库的记录
git reset HEAD^1 --hard   // 重置为上一个 commit,并清空工作区域的代码改动
git reset HEAD^1 --soft // 重置为上一个 commit,不过会保留工作区域的代码改动
  • git revert - 适用于已经提交到远程仓库的记录的撤销

commit message 规范

# 主 type
feat: 增加新功能
fix: 修复bug

# 其他 type
docs: 只改动了文档相关的内容
style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
build: 构造工具的或者外部依赖的改动,例如webpack,npm
refactor: 代码重构时使用
revert: 执行git revert打印的message
test: 添加测试或者修改现有测试
perf: 提高性能的改动
ci:CI(持续集成服务)有关的改动
chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动

本地仓库 <--> 远程仓库

同步

  • git fetch
  1. 从远程仓库下载本地仓库中缺失的提交记录
  2. 更新远程分支指针

提交

  • git push

    在多人协作的分支上尽量不用 git push -f、git rebase、reset 的操作

git push -f.gif

撤销

HEAD 总是指向当前分支上最近一次提交记录。

git checkout <commit_hash> // 让 HEAD 指向指定的 commit
HEAD^ // 向前移动一个提交记录
HEAD^2 // TODO 移动到前面一排记录的第二个提交记录
HEAD~3 // 向前提交三个记录

通过 cat .git/head 可以查看 HEAD 指向

分支

  • git branch - 创建本地分支
git branch '' // 创建分支
git switch '' // 切换分支
git checkout -b '' // 创建分支并切换到该分支
git branch -f <branch> <commit_hash/branch> // 将指定 branch 的 HEAD 指向某个提交记录或者某个分支的 HEAD

stash

git stash 可以将当前工作状态(WIP,work in progress)临时存放在 stash 列表中,待 pull / merge 操作完成后,再从 stash 中重新应用这些修改。

git stash list
git stash save 'message' // 存储一个自定义 message 的 WIP 到 stash 栈中
git stash -u save '' // -u 参数表明新增的文件也一起 stash
git stash pop // 恢复上一次的 WIP 状态,并从 stash 栈中移除
git stash pop stash@{num} // 恢复指定编号的 WIP,并从 stash 栈中移除
git stash apply stash@{num} // 恢复指定编号的 WIP,但不从 stash 栈中移除

如果 git stash 的文件刚好被其他提交修改过了,那么在 pop 时会自动将修改过的文件标记为 conflict 状态

合并代码

merge

该命令主要按以下两种策略合并代码:

Fast-forword(--ff)

git_merge_ff.gif

no-fast-forword(--no-ff)

git_merge_noff.gif

rebase

Rebase 实际上就是取出一系列的提交记录,将他们复制到其他地方。相比 merge,rebase 能提供更加线性的提交历史。这个操作比较危险,会改变提交历史,因此建议不要对已经处于远端的多人共用分支做 rebase 操作。

reword:修改提交信息;
edit:修改此提交;
squash:将提交融合到前一个提交中;
fixup:将提交融合到前一个提交中,不保留该提交的日志消息;
exec:在每个提交上运行我们想要 rebase 的命令;
drop:移除该提交。

git_rebase.gif

复制记录

  • cherry-pick - 可以复制指定的 commit

查看日志

  • git config
// 1. git 配置 alias
git config --global alias.ls 'log --name-status --oneline --graph'
git ls
git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"
git lg
// 2. 用 zsh 配置 alias

git show <commit_id> // 查看具体某次commit记录
git log -p -n // n是代表最近几次的提交,可以查看最近几次commit记录

开发实践

合并多个 commit

  • git rebase -i HEAD~3 // 编辑最近三个记录
p HEAD1
s HEAD2
s HEAD3

如何修改之前的某个提交(已 push)?

  1. git rebase 先调整待修改的提交到最新
  2. 修改后提交
  3. git rebase 再调整回来

如何修改之前的某个 commit?

  • git commit --amend -- 修改 commit message 或追加文件改动建议用这个
  • git reset HEAD^1 --soft

如何复制其他分支的文件夹到当前分支?

git checkout <branch> -- [file_name]

参考

· 13 min read

不论是 react 还是 vue 脚手架,都依赖 webpack 进行构建和打包。这一篇主要是我之前在入门 webpack 4 的时候记的一点小笔记,详细内容还得参见官方文档

简单打个包

初始化项目,会生成一个 package.json

npm init
// 快速 init
// 1、默认都 yes
npm init -y
// 2、设置 init 模板
npm config set init.author.name "JacksonZhou"
npm init -y // 或者 -f

安装 webpack 和 webpack-cli

npm install webpack -D
npm install webpack-cli -D

我们随意创建一个 src 目录下的 index.js 文件,然后在 package.json 修改 scripts

"build": "webapck --mode production"
npm run build

这时会生成一个 dist 文件夹,这个里面就是 webpack 打包好的文件

创建配置文件

创建一个 webpack.config.js,执行 webpack 命令时会去执行这个配置文件。若是自定义配置文件的名字,则应该使用命令 webpack config 文件名

entry

入口,webpack 会从这里开始解析依赖。默认会按 entry 来划分 chunk

entry: './src/index.js'

output

webpack 最终构建出来的静态文件,可配置输出结果的文件名、路径等

const path = require('path') // 本质上webpack配置文件就是nodejs脚本,所以可以使用nodejs的模块
output: {
publicPath: '', // 输出资源的网址前缀
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // 原文件名
}

关于 filename 的 hash 命名中 hash、chunkhash、contenthash 的区别

  • hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值,但是 chunkhash 下,引用其他文件的文件一旦修改,也会改变其他文件的 hash 值,即使其他文件没动过。
  • contenthash保证文件内容必须要修改后,hash 值才会变化

loader

通过使用不同的 loader 来解析处理不同类型的文件。webpack 默认支持打包 js 文件,其他格式的文件需要借助对应的 loader 进行转换

换句话说,当 webpack 遇到其他文件的时候,会去配置文件的 module 选项中去找相应的规则。比如在配置文件中我们可以规定当 webpack 遇到图片文件的时候,就使用 file-loader 来帮我们进行打包文件

常见 loader

  1. babel-loader 将 es6/es7 转换成 es5(待确认)
npm i babel-loader @babel/core @babel/preset-env @babel/polyfill -D
npx babel src --out-dir lib // 测试 babel
module: {
rules: [
{
// 命中 js 文件
test: '/\.js$/',
// 使用 babel-loader 来解析 js 文件
loader: 'babel-loader',
// 只命中 src 目录下的 js 文件,加快 webpack 的编译速度
include: path.resolve(__dirname, 'src'),
// 排除 node_modules,加快 webpack 的编译速度
exclude: /node_modules/,
}
]
},

这样还不能发挥 Babel 的作用。在项目根目录下创建一个 .babelrc 文件,添加代码:

{
"presets": [
"@babel/preset-env"
]
}
  1. 构建 CSS
npm i css-loader style-loader -D
module: {
rules: [
{
test: /\.css/,
include: [
path.resolve(__dirname, 'public/style'),
],
use: ['style-loader','css-loader']
// loader 的执行顺序是从下到上,从右到左
}
]
}
// 在index.js里引入
import '../public/style/index.css'

css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,帮我们分析出几个 css 文件之间的关系,并最终把这些 css 文件合并成一段 css,例如解析 @import 和 url() 等引用外部文件的声明;

style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

经由上述两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js 一起打包了。

  1. 处理预处理语言
npm i less-loader -D
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
'less-loader',
],
}),
},
],
},
  1. 处理图片

file-loader 可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回

url-loader会将引入的图片编码,生成 dataURl 并将其打包到文件中。但是如果图片较大,编码会消耗性能。因此 url-loader 提供了一个 limit 参数,小于 limit 字节的文件会被转为 DataURl,大于 limit 的还会使用 file-loader 进行 copy

image-webpack-loader 可用于压缩图片

{
test: '/\.(png|gif|jpe?g)$/i',
use: [
{
loader: 'url-loader',
options: {
limit: 8192 // 单位是 Byte,一般当文件小于 8KB 时作为 DataURL 处理
}
}
]
},
{
test: '/.*\.(gif|png|jpe?g|svg|webp)$/i',
use: [
{
loader: 'file-loader',
options: {}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: { // 压缩 jpeg 的配置
progressive: true,
quality: 65
},
optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
enabled: false,
},
pngquant: { // 使用 imagemin-pngquant 压缩 png
quality: '65-90',
speed: 4
},
gifsicle: { // 压缩 gif 的配置
interlaced: false,
},
webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
quality: 75
},
}
},
]
}

使用html-withimg-loader进行处理 img 标签引入的图片

npm i html-withimg-loader --save-dev
{
test:/\.(html|htm)$/,
use:'html-withimg-loader'
}
  1. 打包图标字体

其他常用 loader

  • px2rem-loader。移动端 css px 自动转换为 rem 的 loader
  • postcss-loader + autoprefixer。在 css 属性上加上相应的浏览器的厂商前缀,如-webkit、-ms、-moz 等
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
// webpack.config.js
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
'postcss-loader',
]
}
  • ts-loader。转换 ts 代码
  • thread-loader。多进程打包资源
  • raw-loader。将文件以字符串的形式导入
  • ...

plugin

在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。比如有以下这些常见的使用场景

清除 dist 文件

npm i clean-webpack-plugin -D
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

plugins: [new CleanWebpackPlugin()];

关联 HTML

  1. webpack 从入口开始构建 JS ,而通常一个 spa 项目都是从一个 html 页面出发的,这时候需要使用 script 标签引用构建好的 JS 文件。
  2. 但是问题来了,这些构建好的 JS 文件名可能会变,比如用了 hash 值作为文件名,所以我们需要通过插件来自动关联 html 文件
npm i -D html-webpack-plugin
// webpack.config.js
...
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 输出文件
template: './public/index.html' // 模板文件
})
]

之后打包npm run build,dist 目录就有了 index.html 了

其他 plugin

  • ProvidePlugin:设置全局变量
  • mini-css-extract-plugin:将 css 从 bundle 文件中提取成一个独立的 css 文件
  • terser-webpack-plugin:压缩 js 的插件,支持压缩 es6 代码
  • copy-webpack-plugin:将文件或者文件夹拷贝到构建的输出目录
  • ...

mode

开启不同的环境,webpack 4 会根据这个参数做不同的处理和优化,具体处理和优化可以参见 https://webpack.js.org/configuration/mode/#mode-development

resolve

该配置用于扩展模块的解析路径,我们可以通过它配置一些 alias

module.exports = {
...
resolve: {
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
'src': path.resolve(__dirname, '../src'),
},
},
};

optimization

在构建后期负责压缩和优化代码,常见的代码分割现在是通过这个配置来控制。optimization.splitChunks 默认是不用设置的。如果 modeproduction,那 Webpack 4 就会开启 Code Splitting,不过只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks'all'

Webpack 4 的 Code Splitting 最大的特点就是配置简单,和基于内置规则自动拆分。内置的代码切分的规则是这样的:

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

一般用默认的配置就行了。如果有特殊的需求,也可以通过下面的 optimization.splitChunks API 定制。

splitChunks

用法示例

chunks: 'all',
name: true,
cacheGroups: {
// 缓存组,会继承和覆盖splitChunks的配置
default: {
// 模块缓存规则,设置为false,默认缓存组将禁用
minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
priority: -20, // 优先级
reuseExistingChunk: true, // 默认使用已有的模块
},
vendors: {
test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
priority: -10,
},
antd: {
name: 'antd', // 单独将 antd 拆包
priority: 15, // 权重需大于其它缓存组
test: /[\/]node_modules[\/](antd|@ant-design)[\/]/,
},
bizcharts: {
name: 'bizcharts', // 单独将 bizcharts 拆包
priority: 10,
test: /[\/]node_modules[\/]bizcharts[\/]/,
},
antv: {
name: 'antv', // 单独将 antv 拆包
priority: 12,
test: /[\/]node_modules[\/]@antv[\/]/,
},
},

sourceMap

sourceMap 其实就是源码和打包文件的映射关系,可以帮助我们快速定位一些代码问题。webpack 4 默认会开启,关键配置为 devtool,下面是它的一些配置对比:

webpack-sourcemap-type.png

  • inline。直接会将 .map 文件直接打包到对应的 js 中去,从而加快相应的速度。使用这个我们会发现,打包出来的文件没有 .map 文件了,而是以 base64 的形式放入了打包的文件中了。
  • cheap。map 文件只会帮你定为到具体的 某一行,并不会把代码定位到 具体的 某一行 某一列,从而加快速度;cheap 还有一个作用,就是这个选项只使针对业务代码,也就是说只能定位到业务代码里面的错误,并不能定位到我们引用的第三方文件(比如说 loader,第三方模块)的错误。
  • module。不仅会帮我们定位 自己的业务代码中的错误,还会同时帮我们定位第三方模块的错误。
  • eval。使用 eval 包裹模块代码,并且存在 //@sourceURL,这个是打包速度最快,性能最好的的一种方式,但是有的时候,对于代码比较复杂的情况,它提示出来的错误可能不够全面。

建议配置

开发环境:cheap-module-eval-source-map

生产环境不建议开启,如果需要定位错误,可以尝试用cheap-module-source-map