起因
为什么想要写这篇呢?
之前写的工作室招新平台,在构建的时候,总是会弹出来 Bundle 大小过大的警告,于是看了一眼 Bundle 里面到底有些啥
非常的离谱,尤其是 @ant-design/icons
, 我只是引用了几个图标,却把整个库都引了进来, 我也不懂为什么 Webpack 的 tree shaking
没有起作用。这个项目我用的是 umi
, 在看了文档进行一番操作之后并没有成功,所以我想脱离 umi
来看看,到底是 umi
的问题,还是 @ant-design/icons
的问题,还是我太菜 的问题。
新建项目
当你需要新建一个 React 项目的时候,第一反应是什么? create-react-app
, create-next-app
还是 yarn create @umijs/umi-app
? 我们似乎已经习惯用这些一键脚手架来配置项目的babel
和webpack
? 但是这些工具是怎么工作的, babel
和 webpack
又是怎么配置的,今天让我们从0开始探索一遍。
mkdir myapp && cd myapp
yarn init
添加依赖
# Babel
yarn add -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript \
@babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
# Webpack
yarn add -D webpack webpack-cli webpack-dev-server \
fork-ts-checker-webpack-plugin html-webpack-plugin babel-loader
# Typescript
yarn add -D typescript @types/node
# React
yarn add react react-dom react-router
yarn add -D @types/react @types/react-dom @types/react-router
简单介绍一下这里的依赖都是干啥的
babel: JS 语法编译器,这里是将 ts 和 jsx 编译到浏览器能用commonjs (其实不用babel, 只用typescript的编译器也可以,但是限制比较多,而且 babel 的插件多)
- @babel/core
- @babel/preset-env: 根据你选择的
target
和其他配置,自动加载 babel 的插件 - @babel/preset-react: React 的相关配置
- @babel/typescript: Typescript 的相关设置
- @babel/plugin-proposal-class-properties : 为 JS 的类成员添加属性
- @babel/plugin-proposal-object-rest-spread : 添加 JS 的剩余参数
Webpack: JS 打包器,主要是将各个模块打包到一个文件里,精简体积,减少文件数量,适合前端使用
- webpack
- webpack-cli
- webpack-dev-server : webpack 的开发服务器,可以实现在上面实现 HMR 等
- fork-ts-checker-webpack-plugin : webpack 的 ts 类型检查插件
- html-webpack-plugin : 创建 HTML 文件
- babel-loader : Webpack 的 Babel Loader, 将 Babel 编译完的文件交给webpack打包
.babelrc
{
"presets": [
"@babel/env",
"@babel/react",
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}
webpack.config.js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const SRC_DIR = path.resolve(__dirname, 'src')
module.exports = {
entry: './src/index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({inject: true, template: path.join(SRC_DIR, 'index.html')}),
new ForkTsCheckerWebpackPlugin()
]
}
tsconfig.json
tsc --init
# 然后修改
- "jsx": "preserve"
+ "jsx": "react"
src/index.html
HTML模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Webpack</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/index.tsx
import React from "react";
import ReactDOM from 'react-dom';
ReactDOM.render(
<h1>Hello, World</h1>,
document.getElementById('root')
);
编译和查看
webpack
# yarn global add serve
serve -s ./dist
# 然后打开浏览器 http://localhost:5000
配置优化
Dev Server
Dev Server
来自我们前面安装的 webpack-dev-server
,正如你所见,前面用的先编译,再通过 serve
提供 http
访问的方式过于繁琐,而 webpack-dev-server
不仅提供了一键编译运行,还提供了热更新等功能。
打开 webpack.config.js
,添加
{
...,
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 3000
}
}
然后 webpack serve
,打开 devServer
同时,还可以向 package.json
里添加 script
{
...,
"scripts": {
"serve": "webpack serve",
"build": "webpack"
},
}
yarn serve
& yarn build
Source Map
Source Map 能够让我们在浏览器更方便的调试,在 webpack.config.js
中添加
{
...,
devtool: process.env.NODE_ENV === "development" ? "source-map" : false
}
然后再 NODE_ENV=development yarn serve
, 打开页面,按F12, 选择 Source 中的 bundle.js, chrome会提示 Source Map Detect, 然后按Ctrl + P 即可选择自己需要的文件查看。
Bundle Analyzer
当我们使用 webpack serve
的时候, Webpack 会亲切的提示我们
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
bundle.js (281 KiB)
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit
(244 KiB). This can impact web performance.
Entrypoints:
main (281 KiB)
bundle.js
但是,我们在上面不过是写了 <h1>Hello, World</h1>
, 为什么 Bundle 的大小会大了这么多呢,这个时候我们就需要使用 webpack-bundle-analyzer
来分析 Bundle 里究竟打包了些什么玩意。
yarn add webpack-bundle-analyzer
然后在 webpack.config.js
里加入
...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
{
...
plugins: [
...,
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZER === '1' ? 'server' : 'disabled'
})
]
}
接着 ANALYZER=1 webpack
,然后就弹出来 Bundle 大小分析页面
可以看到绝大部分都来自 react
的包,那么我们如何精简这个 Bundle 呢? 那就要用到
Externals
从 React 的教程上我们可以看到, React 可以直接从 <script></script>
里面引入,那么我们如果直接从 <script></script>
去引入 React, 是不是就可以精简 Bundle 的大小了呢?
这个时候我们就可以用到,它可以让某些包在打包的时候被排除出去,比如我们这里提到的 React
在 webpack.config.js
里面添加
{
...,
externals: {
react: 'window.React',
'react-dom': 'window.ReactDOM'
}
}
这样就够了吗? 不, 如果你直接编译查看,浏览器会爆
Uncaught TypeError: Cannot read property 'render' of undefined
at Module../src/index.tsx (index.tsx:4)
at __webpack_require__ (bootstrap:21)
at startup:4
at startup:6
为什么会这样呢,如你所见,上面externals
是指,打包时不引入 react
和 react-dom
, 而是从 window.React
和 window.ReactDOM
中引入,从编译出来的代码上来看,就是
module.exports = window.Reat;
module.exports = window.ReactDOM;
但是我们并没有引入 <script></script>
来定义这两个全局变量, 那如何引入呢?
最简单的方式当然修改src/index.html
,在其上添加,但是我觉得这样并不优雅,我想要在 webpack.config.js
里解决这些问题,那应该怎么做呢,不知道你是否还记得,我们前面提到的一个插件
html-webpack-plugin
这个插件就是用来根据模板修改html页面的,在使用它之前,我们先把 index.ejs copy 下来,并放到 src/index.ejs
中,ejs 是一个嵌入式 JavaScript 模板引擎,某种意义上来说和 jsx
有点像,这里我们主要用它来做模板。
修改 webpack.config.js
{
...,
plugins: [
...,
new HtmlWebpackPlugin({
inject: true,
template: path.join(SRC_DIR, 'index.ejs'),
scripts: [
process.env.NODE_ENV === 'development' ?
'https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js' :
'https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js',
process.env.NODE_ENV === 'development' ?
'https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.development.js' :
'https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js'
],
lang: 'zh_CN',
appMountIds: ['root']
}),
]
}
经过上面这些配置,现在我们再看看 bundle.js
我们成功的将 bundle.js
缩小到不到 1kb!
CSS
上面我们演示的是一个非常简单的程序,页面的代码只有一个 <h1>Hello, World</h1>
, 在实际应用中我们肯定还会用到css
,我们如何打包css
呢?
yarn add -D style-loader css-loader
- style-loader: 读取
css
并且插入到<head></head>
中 - css-loader: 对css中的
@import()
和url()
解析
需要注意的是,如果你需要在tsx
中import xxx.css
,你需要创建src/react-app-env.d.ts
,并写入
declare module '*.css';
然后我们修改 webpack.config.js
{
...,
module: {
rules: [
...,
{
test: /\.css$/,
use: [
{loader: "style-loader"},
{
loader: "css-loader",
options: {importLoaders: 1}
}
]
}
]
}
}
index.css
.container {
display: block;
margin-top: 100px;
text-align: center;
background-color: cadetblue;
color: black;
font-size: 48px;
}
index.tsx
import React from "react";
import ReactDOM from 'react-dom';
import './index.css';
ReactDOM.render(
<div className={'container'}>
<p>Hello, World</p>
</div>,
document.getElementById('root')
);
编译后可以看到css生效了。
上面的配置是将css写进·bundle.js
,但是习惯上我们会将css
和js
分开来,如何将css
单独打包出来呢
yarn add -D mini-css-extract-plugin
然后修改webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 要把上面添加的style-loader删了
{loader: MiniCssExtractPlugin.loader},
{
loader: "css-loader",
options: {importLoaders: 1}
}
]
}
]
},
plugins: [
...,
new MiniCssExtractPlugin()
]
}
编译运行,可以看到css被打包进了了dist/main.css
Public Folder
我们的网页可能需要一些静态文件,比如favicon
,如何让webpack
在打包的时候复制静态文件到/dist
呢?
yarn add -D copy-webpack-plugin
然后修改webpack.config.js
module.exports = {
...,
plugins: [
new CopyPlugin({
patterns: [{from: './public',}]
}),
...
]
}
Hash
网页比起App有个好处,它没有版本更新的概念,可以让用户使用到最新的版本。
但是有个问题,大多数网站都会使用CDN,而CDN有缓存,我们的bundle.js
很可能会被CDN缓存,让用户无法获取最新的版本,手动清除缓存或者不缓存bundle.js
都不是最优解决方案,那怎样解决最好呢?
webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js'
}
}
关于这里的明明规则,请看 Template strings
Tree shaking
本来是想提一下的,但是发现 Webpack 5
的 Three shaking
似乎比 4 要好的多,就先🕊了吧。
Chunks Split
前面我们所配置的webpack
,都是将代码打包到一个文件里,但是实际网页中,单文件形式的包往往过大,非常影响体验,所以我们要对生成的文件进行分割,将不在首页加载的包分离出来,提高首屏加载时间。
optimization: {
splitChunks: {
cacheGroups: {
react: {
name: 'react',
chunks: "all",
test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom|moment|antd|@ant-design)[\\/]/,
priority: 12
},
utils: {
name: 'utils',
chunks: "all",
test: /[\\/]node_modules[\\/](lodash|ramda|refractor|axios)[\\/]/,
priority: 11
},
charts: {
name: 'charts',
chunks: "all",
test: /[\\/]node_modules[\\/]recharts[\\/]/,
priority: 11
},
vendor: {
name: 'vendor',
chunks: "all",
test: /[\\/]node_modules[\\/]/,
priority: 10
}
}
}
}
Dynamic Import
import React from 'react';
const ComponentA = React.lazy(() => import("./ComponentA"));
...
import React, {Suspense} from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import ComponentA from 'components';
const App = () => {
return (
<BrowserRouter>
<Switch>
<Suspense fallback={<h1>Loading...</h1>}>
<Route path='/a' component={<ComponentA />} />
</Suspense>
</Switch>
</BrowserRouter>
)
}
Server side render
SSR
我感觉可以单独拎出来再水一篇博客, 就下次再说吧。
0 条评论