手摸手踩坑react-template

注意:为了方便统一,本文中出现的安装包依赖的方式均使用 yarn 来安装

采取最简便的方式

如果说搭建一个 react template 最简便的方式,那便是使用官方的 cli,即 create-react-app,因为这边还需要使用 typescript 技术栈,所以在搭建的过程中可以加一个参数 --template typescript,表示使用的模板是 typescript 的模板

yarn create react-app my-app --template typescript

然后在项目的目录中执行如下的命令安装依赖

yarn add typescript @types/node @types/react @types/react-dom @types/jest

然后生成的目录结构基本上如下所示

# my-app
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── setupTests.ts
├── tsconfig.json
└── yarn.lock

其中比较重要的就是 src 以及 tsconfig.json 文件,一个用来存放源码,一个用来定义 tsconfig.json 文件。但这两个文件并不是在最开始搭建的时候一个大障碍,一个比较大的障碍是,使用 cra 创建的项目,如果以后项目的需求变更,就必须使用 config-overide.js 去做一个覆盖原本默认 webpack 配置的配置文件。同时你还需要下载 react-app-rewired 这个库来重新构建整个项目。考虑到 react-app-rewired 社区中维护的插件/包/库都不是特别的多,并且使用这种过度包装的 package 也比较黑盒,难于调试,所以最后决定使用 yarn eject 抛出该项目的 webpack 配置,手动实现真正自定义的配置和其他的操作(做注释以及方便后面的维护)

yarn eject

执行 yarn eject 之后,基本上就多了两个文件夹 scripts 以及 config

# scripts
├── build.js
├── start.js
└── test.js

其中的 build.jsstart.jstest.js 分别对应 package.json 文件中的 scripts 脚本

// package.json
{
// ...
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},
// ...
}

道理我都明白,start 对应启动一个本地的 devServer 进行调试,而 build 对应打包最终的文件,test 对应代码的测试脚本,但是你特么的为啥要引入这么多个包啊?全部都是函数套函数的形式,你这谁看的懂啊?

# config
├── env.js
├── getHttpsConfig.js
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── modules.js
├── paths.js
├── pnpTs.js
├── webpack.config.js
└── webpackDevServer.config.js

还有这个所谓的 eject 出来的 webpack 的配置,粗略看了下核心文件 webpack.config.js 中光是配置文件的行数就有 670 行,尽管中间参杂了各种英文的注释,但是对于一个需要配置的新人来说,还是过于繁杂了,光是看到它引入的各种插件也是头疼无比,其实到这个时候,就已经有不详的预感了(对于如此复杂的一个配置,任何修改和添加或者删除都将是灾难性的)

sass-loader 的 bug

因为在之前的 xp 项目中(比如 xp-homework),可以通过 .env 中的环境变量给一个 scss 变量赋值并追加到某个定义好的 scss 文件中,在 xp-homework 项目中的 vue.config.js 有这样的代码

module.exports = {
...
css: {
loaderOptions: {
sass: {
data: `@import "@/styles/variables.scss"; $userSelect: ${process.env
.VUE_APP_USER_SELECT || 'none'};`
}
}
},
...
}

参考 webpack 中有关 sassloader prependData 属性的文档,里面的解释如下

Prepends Sass/SCSS code before the actual entry file. In this case, the sass-loader will not override the data option but just append the entry’s content.

This is especially useful when some of your Sass variables depend on the environment

以及里面的函数配置的写法

{
loader: 'sass-loader',
options: {
prependData: (loaderContext) => {
// More information about available properties https://webpack.js.org/api/loaders/
const { resourcePath, rootContext } = loaderContext;
const relativePath = path.relative(rootContext, resourcePath);
if (relativePath === 'styles/foo.scss') {
return '$value: 100px;';
}
return '$value: 200px;';
},
},
}

将项目中的 webpack.config.js 中如下的代码

...
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;

改写成

if (preProcessor) {
const loader = {
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
}
}
if (preProcessor === 'sass-loader') {
loader.options.prependData = (loaderContext) => {
const { resourcePath, rootContext } = loaderContext
const relativePath = path.relative(rootContext, resourcePath)
if (relativePath === 'src/styles/base.scss') {
return `$userSelect: ${process.env.REACT_APP_USER_SELECT || 'none'}`;
}
}
}
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap
}
},
loader
)
}
return loaders

使用 yarn start 后报如下的错误

就非常的奇怪,一个比较 trick 的解决方式是,将所有需要引入到 .scss 文件的地方,都改为 .sass,并将其语法也改写成 sass 语法。编译通过。不过这并不是一个好的解决方式,因为定位的问题应该是 sass-loader,可能还有其他的配置,于是去 github 上找了 sass-loader 的相关配置,找到了这么一段话

但是看了下,项目中其实是安装了 sass 的,按照道理来说应该会自动使用 sass 来 implementation 才对,但是这里又报错,于是尝试去掉 sass,直接安装 node-sass,问题居然解决了!!!所以如果以后需要用到 sass-loader 的地方,最好还是先安装 node-sass 比较稳妥

路径 alias 引入失败

之前在搭建模板的过程中,只是在 tsconfig.json 文件中设置了 baseUrl 以及 paths

{
"baseUrl": "./src", // 解析非相对模块名的基准目录
"paths": { // 这个属性就是用来填写编译时对应的 alias 用的
"@": ["./src"]
}
}

如果是在 webpack 中不做设置,那么在编译时 webpack 会报错,所以在该项目中的 webpack.config.js 中也需要做 alias 的配置

alias: {
'@': path.resolve(__dirname, '../src'),
}

但是设置完后编译,typescript 表示依然找不到模块,这个时候就是 tsconfig.json 配置的问题了。如果你的 baseUrl 中已经写了 ./src,那么 paths 应该这么写

{
"baseUrl": "./src", // 解析非相对模块名的基准目录
"paths": { // 这个属性就是用来填写编译时对应的 alias 用的
"@/*": ["."]
}
}

@/* 表示匹配代码中的以 @/xxx/yyy 这种形式引入模块,由于已经设置了 baseUrl,所以这里 paths 中的配置就自动加上了 baseUrl了。比如引入一个包 import XXX from '@/xxx/yyy',而你的配置为 "@/*": ["."] 的话,它会自动在 ./src/xxx/yyy 下去寻找这个模块。另外为了保持格式,我将对应的 webpack 的 alias 以及 tsconfig 的 alias 都做了一一对应,如下

// webpack.config.js
alias: {
// 设置 webpack 编译时的 alias
// ../ 目录的原因是因为当前 src 目录相当于当前 webpack.config.js 的就是上级目录
'@': path.resolve(__dirname, '../src'),
'@api': path.resolve(__dirname, '../src/api'),
'@assets': path.resolve(__dirname, '../src/assets'),
'@components': path.resolve(__dirname, '../src/components'),
'@router': path.resolve(__dirname, '../src/router'),
'@styles': path.resolve(__dirname, '../src/styles'),
'@utils': path.resolve(__dirname, '../src/utils'),
'@views': path.resolve(__dirname, '../src/views'),
}
// tsconfig.json
{
"baseUrl": ".", // 解析非相对模块名的基准目录
"paths": { // 这个属性就是用来填写编译时对应的 alias 用的
"@/*": ["./src/*"],
"@api/*": ["./src/api/*"],
"@assets/*": ["./src/assets/*"],
"@components/*": ["./src/components/*"],
"@router/*": ["./src/router/*"],
"@styles/*": ["./src/styles/*"],
"@utils/*": ["./src/utils/*"],
"@views/*": ["./src/views/*"],
}
}

引入 svg 报错

虽然现在能够以 img 标签 src 方式引入 svg,如下

import React from 'react
import logoPng from '@assets/logo.png'

class Demo extends React.Component {
return (
<div>
<img src={logoPng} alt="logo" />
</div>
)
}

但是我们更希望能将 svg 当成一个组件的方式引入,如下

import React from 'react
import Logo from '../../assets/logo.svg'

class Demo extends React.Component {
return (
<div>
<Logo />
</div>
)
}

最开始是参考 Adding SVGs 这篇文章,它的解决方案如下

import { ReactComponent as Logo } from './logo.svg';
function App() {
return (
<div>
{/* Logo is an actual React component */}
<Logo />
</div>
);
}

试了下发现是不行的,然后继续搜集线索,发现有个 babel 的插件可以解决这个问题,即 babel-plugin-inline-react-svg,而这是需要在 babel-loaderoptions.plugins 进行配置,如下

oneOf: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
...
loader: require.resolve('babel-loader'),
options: {
...
plugins: [
require.resolve('babel-plugin-inline-react-svg'), // 这里只是需要添加这个插件即可
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]'
}
}
}
]
],
},
...
]

在说 babel-plugin-inline-react-svg 这个插件的原理之前,先说下 babel-plugin-named-asset-import 的原理,这个插件本质上会将如下的语句

import { url as logoUrl } from './logo.png';
import { ReactComponent as Icon } from './icon.svg';

转换成

import logoUrl from 'url-loader!./logo.png';
import Icon from 'svgr/webpack?-svgo,+titleProp,+ref!./icon.svg';"

但是由于这个插件对 svg 的处理,只能是通过 img 的 src 标签引入,所以才需要 babel-plugin-inline-react-svg 这个插件,能把 svg 自动封装成一个组件来使用,原理应该是 babel-loader 在对文件进行处理时,对源码做了一些修改导致的

新的问题

但是紧接着又带来了新的问题,在使用 alias 的方式引入 svg 时,会报错,即 svg 文件对应的路径找不到

import Logo from '@assets/logo.svg'
...

出现这个问题的原因,可能在于 babel-loader 的 plugin 那里,之前我有提到过 babel-plugin-named-asset-import 这个插件的工作原理,有可能是 webpack 在处理 alias 之前,优先被这个插件做了处理了,即以上的代码会被转换成

import Logo from 'svgr/webpack?-svgo,+titleProp,+ref!@assets/logo.svg'
...

这个时候 svgr/webpack 这个 loader 自然就找不到路径为 @assets/logo.svg 的这个模块,于是编译就会报错了。

所以更好的方式?

由以上的方式,我们就懂得了一个道理,最 easy 的方式,有的时候往往也是最 hard 的。且不说上面的 svg 引入的问题没有得到完美的解决,要是以后项目逐渐的变得复杂了,需要对项目的基建做修改和升级呢?出了问题之后要怎么维护呢?连现在这个简单的问题的解决方案都如此麻烦的情况下,那万一以后出了更加匪夷所思的问题需要将项目基建整体重构呢? …etc

基于以上的风险考虑,从项目的可读性和可维护性着想的话,也许最好的方式,就是自己手撸一个 template 出来

重构 config

进行重构的操作之前,可以先将依赖包进行删除(包括 package.json 中的),然后执行如下安装基础包

# 安装 typesript
yarn add -D typescript

# 安装 react 相关包
yarn add react react-dom react-router-dom
yarn add -D @types/react-router-dom @types/react-dom @types/react

# 安装 babel 相关包
yarn add -D
@babel/core # babel 核心
@babel/plugin-proposal-class-properties # 支持 class xxx { a = 'xxx' } 这样在类中定义属性
@babel/plugin-proposal-decorators # 支持使用装饰器
@babel/plugin-syntax-dynamic-import # 支持异步加载(improt (xxx))
@babel/preset-react @babel/preset-typescript # 支持将 react 以及 ts 编译成 js
babel-loader
babel-plugin-import # 若是需要 antd 的包,可以安装这个进行按需加载

# 安装 html-webpack-plugin
yarn add -D html-webpack-plugin

# 安装 sass 以及 style 配置相关包
yarn add -D sass-loader node-sass style-loader css-loader css-modules-typescript-loader

# 安装 url-loader
yarn add -D url-loader

# 安装 @svgr/webpack
yarn add -D @svgr/webpack

然后需要做的就是删除 config 文件夹下的各种 js 文件,然后新建几个文件

# config
├── devServerConfig.js # webpack devServer 相关配置
├── devServerProxyConfig.js # webpack devServer proxy 相关配置
├── pluginsConfig.js # webpack plugins 相关配置
├── resolveConfig.js # webpack resolve 相关配置
├── rules # webpack rules 相关配置
│ ├── assetsRules.js # assets 资源文件相关 rules
│ ├── fileRules.js # file 文件相关 rules
│ ├── jsRules.js # js 相关 rules
│ └── styleRules.js # style 相关 rules
├── utils.js # 工具库 js,将一些可能用到公共的函数放到这个里面
└── webpack.config.js # webpack 配置

从整体上看,webpack.config.js 的配置文件只有寥寥几行

const { resolve } = require('./utils')
const jsRules = require('./rules/jsRules')
const styleRules = require('./rules/styleRules')
const fileRules = require('./rules/fileRules')
const assetsRules = require('./rules/assetsRules')
const pluginsConfig = require('./pluginsConfig')
const resolveConfig = require('./resolveConfig')
const devServerConfig = require('./devServerConfig')

/**
* @type {import('webpack').Configuration}
*/
module.exports = {
entry: {
app: resolve('src/index.tsx')
},
output: {
path: resolve('dist'),
filename: '[name].js'
},
devServer: devServerConfig,
resolve: resolveConfig,
plugins: [...pluginsConfig],
module: {
rules: [
...jsRules,
...styleRules,
...fileRules,
...assetsRules
]
}
}

结构非常清晰了,相信大家应该都对这个配置挺熟悉了,所以下面就重点来详细说说这几个文件

utils.js

源码如下

/**
* @title utils.js
* @description 这个文件是为了方便路径处理用的
*/
const path = require('path')

exports.resolve = dir => {
return path.join(__dirname, './../', dir)
}

因为这里是用 node 启动的,所以我们可以将其当成一个 commonjs 模块,exports.resolve 表示暴露出一个叫 resolve 的模块给外部,里面的逻辑就是将一个 dir 的名字拼凑成上级目录并返回。因为很多地方会用到,所以这里封装成一个模块比较好

jsRules.js

源码如下

/**
* @title jsRules.js
* @description 定义 js 规则
*/

const { resolve } = require('../utils')

module.exports = [
{
// 这里编写 babel 对 .tsx 文件编译的配置
test: /\.(j|t)sx?$/, // 正则匹配以 .jsx 或者 .tsx 结尾的文件
include: resolve('src'), // 表示限定范围在 src 目录下
use: [
{
loader: 'babel-loader',
options: {
babelrc: false, // 这里不用 babelrc 文件
presets: ['@babel/preset-typescript', '@babel/preset-react'],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }], // 支持使用装饰器语法
['@babel/plugin-proposal-class-properties', { loose: false }], // 非宽松模式,类属性编译成赋值表达式,而不是 Object.defineProperty 这种形式
'@babel/plugin-syntax-dynamic-import' // 支持动态引入 import
]
}
}
]
}
]

为什么不使用 ts-loader 以及 awesome-typescript-loader? 是因为 babel 的编译速度更快

styleRules.js

源码如下

/**
* @title styleRules.js
* @description 定义 style 规则
*/

const { resolve } = require('../utils')

module.exports = [
{
// 支持 scss/sass
test: /\.(scss|sass)$/,
use: [
'style-loader',
'css-modules-typescript-loader', // 在编写或改动了scss文件后,这个插件会自动生成 xxx.scss.d.ts 文件
{
loader: 'css-loader',
options: {
modules: { // 支持 css-module
localIdentName: '[local]_[hash:base64:10]'
}
}
},
{
loader: 'sass-loader',
options: {
includePaths: [ // 添加公共样式文件路径,这里 sass-loader 的版本为 7.3.1,请勿瞎升级!!!
resolve('src/styles')
]
}
},
]
},
{ // 支持 less
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'less-loader',
options: { // 这里 javascriptEnabled 一定要为 true
javascriptEnabled: true
}
}
]
}
]

这里需要注意的是,如果不做特殊的设置,webpack 的调用 loader 的顺序是从右往左的,即对于上面的支持 sass 的 loader 来说,它的执行顺序如下

  1. sass-loader: 将 sass|scss 文件编译成 css
  2. css-loader: 将 css 转化为 commonjs 模块,比如 css 中有 url(./xxx) 的将其转化成 require(./xxx)
  3. css-modules-typescript-loader: 在编写或改动了scss文件后,这个插件会自动生成 xxx.scss.d.ts 文件
  4. style-loader: 将经过 css-loader 转化成的 js 字符串转化成 style 的内容然后挂在 dom 上

为什么需要 css-modules-typescript-loader 这个 loader 呢,因为对于 react 来说,我们一般通过这种方式使用一个 css 文件的类

// index.tsx
import React from 'react'
import style from './index.scss'

const App = () => {
return <div className={style.xxx}>你好</div>
}

export default App

这个时候不做设置,index.tsx 文件会报错说找不到这个 index.scss 模块。这是因为 ts 不认识这个 index.scss 文件,这个时候就需要加 .d.ts 写对应的类型文件才行(毕竟是 ts 的项目。。。)

可以自己在 src/types 目录下新建这个文件,然后写上

declare module '*.scss' {
const content: any
export = content
}

这个时候就可以了,当然我们可以想,不可能每个 scss 文件都这样做,能不能做的自动化一点,可以的,就使用 css-modules-typescript-loader 这个 loader 来帮我们做事情

那么这里做的 css-module 有什么好处呢?就是 react 中的 css 的模块化local 表示 类名[hash:base64:10] 表示这是一串由 base64 码生成的 hash,并且这个码的长度有 10 位。那么在浏览器上对应的 dom 上就会生成对应的类,这个对应的类下就有对应的 css 了,就形成来 css 的模块化,也就是自行隔离

xxx_1oiiefdai {
background: red;
}

fileRules.js

源码如下

/**
* @title fileRules.js
* @description 定义 file 输出文件的规则
*/

const path = require('path')
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
)

module.exports = [
{
test: /\.(png|jpe?g|gif|bmp)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: imageInlineSizeLimit,
// 在任意操作系统上使用 POSIX 文件路径时获得一致的结果
name: path.posix.join('static/media', '[name].[hash:8].[ext]')
}
}
]
}
]

这里主要是对 png 等图片格式的处理,这里注意 limit 属性配置,这个属性配置是用来指定文件的最大的 byte 也就是字节数的,若文件的大小超过定义的字节数,那么 webpack 就会使用 file-loader 去处理文件,并且所有的查询参数都会传递给 file-loader。这里在使用 url-loader 时不需要安装 file-loader,因为它本身就依赖 file-loader。name 属性配置表示最终生成的目标资源文件的路径以及名字

assetsRules.js

源码如下

/**
* @title assetsRules.js
* @description 定义资源的规则
*/

module.exports = [
{
// 支持本地 svg 导入
test: /\.svg$/,
use: ['@svgr/webpack'],
}
]

终于来了!只要单纯使用这个 loader,便可以解决上文中提到的本地以组件方式引入 svg 的问题!没有其他任何的幺蛾子!非常简单的配置!!!(这里当然也顺便解决了 alias 引入的问题)

pluginsConfig.js

源码如下

/**
* @title plugins.js
* @description 这个文件是为了存放 webpack 插件用的
*/

const HtmlWebpackPlugin = require('html-webpack-plugin')
const { resolve } = require('./utils')

module.exports = [
new HtmlWebpackPlugin({
template: resolve('public/index.html'),
inject: true // script 注入到 template 的 body 下
})
]

html-webpack-plugin 一个耳熟能详的插件了,本质上的作用就是将生成的 js 生成 script 标签并且注入到模板页面的 body 中

当然在这个文件中统一做插件的处理,这样就能够方便的管理配置

resolveConfig.js

源码如下

/**
* @title aliases.js
* @description webpack 中需要配置的 alias
*/
const { resolve } = require('./utils')
module.exports = {
extensions: ['.js', '.jsx', '.ts', '.tsx'], // 定义了文件的下标之后,以后 import js 模块的时候可以不用加后缀
alias: { // 定义 alias
'@': resolve('src'),
'@api': resolve('src/api'),
'@assets': resolve('src/assets'),
'@components': resolve('src/components'),
'@router': resolve('src/router'),
'@styles': resolve('src/styles'),
'@utils': resolve('src/utils'),
'@views': resolve('src/views'),
}
}

这个文件的配置也很简单,基本就是配置了下 extensions 以及 alias 属性,都是为了方便写代码做的配置。不过这里需要注意的是,对于 extensions 配置,如果引入的是同名的文件,比如在某个文件夹下有两个文件 xxx.jsxxx.jsx,你这边做引入了 import xxx from 'xxx',那么这个时候 webpack 就会采用你 extensions 中配置的第一个后缀名,剩下的不要,也就是说你最终引入的就是 xxx.js

devServerConfig.js

源码如下

/**
* @title devServerConfig.js
* @description devServer 配置
*/

const proxy = require('./devServerProxyConfig')

const host = process.env.HOST || '0.0.0.0'
const port = process.env.PORT || 3000

module.exports = {
compress: true, // 为所有 served 的文件启用 gzip 压缩
clientLogLevel: 'debug', // 设置 log 等级,可设置为 'silent' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'none' | 'warning'
hot: true, // 开启热重载
host,
overlay: false, // 出现编译器错误或警告时,在浏览器中显示全屏覆盖。 如果只想显示编译器错误就置为 true
port,
proxy,
quiet: false, // 允许 errors 或者 warnings log
}

注释都写的很清楚了,其实都是些基本的配置

devServerProxyConfig.js

源码如下

/**
* @title devServerProxyConfig.js
* @description devServer proxy 代理配置
*/

module.exports = {
'/xhb_api': {
target: 'https://demo.xxx.cn',
changeOrigin: true, // 默认情况下,代理时会保留主机头的来源,您可以将 changeOrigin 设置为 true 来覆盖此行为。
pathRewrite: {
'^/xhb_api': '/api'
}
},
'/api': {
target: process.env.REACT_APP_API_DOMAIN,
changeOrigin: true
}
}

代理配置,跟之前 vue.config.js 中的代理配置很像的,上面第一个代理配置表示 请求 /xhb_api/users/xxx 类似的接口时,请求本来会被代理到 https://demo.xxx.cn/xhb_api/user/xxx 这个上面来,但是因为你写了 pathRewrite,中间的 /xhb_api 会被替换成 /api,也就是说最终的代理地址为 https://demo.xxx.cn/api/user/xxx

微小的总结

至此为止,一个基本的基于 react 以及 typescript 的项目就搭建完成了,后续的部分就是对模板进行些许优化的步骤了。不过总体来看,比之前使用 cra eject 的配置清爽了不少,并且由于配置的模块化(而不是函数嵌套似的模块化),也使得项目本身易于阅读和拓展

优化配置

构建加速与构建缓存

首先需要安装两个 loader

yarn add -D cache-loader thread-loader

一般使用 cache-loader 构建缓存,用 thread-loader 构建加速。首先创建 config/loaders.js 文件。然后源码如下所示

 * @description 这个文件是为了设置构建缓存和构建加速用的
*/

const { resolve } = require('./utils')

// 构建缓存
const cacheLoader = {
loader: 'cache-loader',
options: {
// 缓存文件路径
cacheDirectory: resolve('.cache-loader')
}
}

// 构建加速,多线程编译
const threadLoader = workerParallelJobs => {
const options = { workerParallelJobs }
Object.assign(options, {
poolTimeout: 2000
})
return {
loader: 'thread-loader',
options
}
}

module.exports = {
cacheLoader,
threadLoader
}

构建缓存的原理就是在本地有一个 .cache-loader 的文件夹,所有的需要缓存的玩意儿都放在这个里面,这样每次构建的时候若变化不大就直接取缓存里面的东西输出,可以加快构建的速度。而构建加速就是开启多线程编译。

因为平常在编译时,需要编译的主要是 js 以及 css 文件,所以应当在 jsRules 以及 styleRules 里引入


这里 less-loader 就没有用到 thread-loader 了,因为会报错

优化打包和抽离 css

优化打包方面主要用到了两个插件

yarn add -D terser-webpack-plugin optimize-css-assets-webpack-plugin
  • terser-webpack-plugin 是用来优化 js 的压缩过程的,这里没有用到 webpack.optimize.UglifyJsPlugin 的原因是该插件不支持 es6 语法,为了方便就直接使用它了
  • optimize-css-assets-webpack-plugin 是用来优化 css 的压缩过程的

再加上 webpack 本身有对代码分割的 optimization 配置,那么一个优化配置文件 config/optimizationConfig.js 的源码就是如下

/**
* @title optimizationConfig.js
* @description 定义 webpack 优化配置
*/

const TerserPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
// 将部分清单代码单独打包出来并命名为 manifest
runtimeChunk: {
name: 'manifest'
},
splitChunks: { // 这是 webpack optimization 的优化配置,具体可以参考官网,本质上就是配置需要分离的包
cacheGroups: {
default: false,
// 抽离 node_modules 下的公共代码
// TODO 这里还可以继续抽离代码,待优化
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'commons',
chunks: 'initial'
},
// 这里还可以为一些其他的包做单独打包的配置,比如
// antd: {
// name: 'antd',
// test: /[\\/]node_modules[\\/](antd)[\\/]/,
// chunks: 'all',
// priority: 9
// },
}
},
minimizer: [
// 优化 js 压缩过程
new TerserPlugin({
// 开启缓存
cache: true,
// 开启多线程
parallel: true,
extractComments: false
}),
// 优化 css 压缩过程
new OptimizeCSSAssetsPlugin({
// 使用 cssnano 压缩,插件自带
cssProcessor: require('cssnano'),
cssProcessorOptions: {
safe: true,
autoprefixer: false,
discardComments: {
removeAll: true
}
},
// 将压缩过程输出到控制台
canPrint: true
})
]
}

css 文件若是都堆积在一坨也会变的很大,所以需要 mini-css-extract-plugin 来对 css 进行抽离

yarn add -D mini-css-extract-plugin

config/pluginsConfig.js 中加上这个插件配置

然后在 styleRules.js 中加上这个 MiniCssExtractPluginLoader(是的,本身这个插件是包含一个 loader 和一个 plugin 的)

这里为啥要对 style-loader 做一个替换呢,因为我们知道在 style-loader 是在页面上添加一个 style 节点的,而分离 css 的步骤应该发生在 css-loader 之后,所以需要做一个替换(也就是说 minicss extract 这个插件内部帮我们做了 style-loader 的事情)

这里需要注意几个 hash 值所表达的意思

  1. hash: hash 和整个项目的配置有关,只要项目中有代码改变,那么所有打包出来的 hash 值都会变,并且所有文件共用一个 hash 值
  2. chunkhash: chunkhash 和 hash 不同点在于,它根据入口文件进行依赖文件解析,然后构建对应的 hash 值,也就是每个打包出来的文件 hash 值都是不一样的,每次修改代码时候,他会根据依赖关系自动修改相关模块的 hash 值,但是打包出来对应的 js 和 css 文件的 hash 会相同。
  3. contenthash: 在打包代码的时候,一般会将 css 文件分离出来,然后我们通常会在组件中引入 css 文件,这时候如果使用的是 chunkhash,在只修改组件 js 代码的情况下因为对应的 css 文件的 hash 值相同,打包出来的 css 文件的 hash 值也会跟着变,这时候就可以使用 contenthash了,他会针对每个文件的内容来计算 hash 值

将 config.json 拷贝到 dist 目录

这个很简单,只需要装一个插件 copy-webpack-plugin

yarn add -D copy-webpack-plugin

然后在 config/pluginsConfig.js 中配置这个插件

支持在代码中使用环境变量

可以使用 dotenv-webpack 这个插件

yarn add -D dotenv-webpack

然后在 config/pluginsConfig.js 中引入这个插件

path 属性表示引入的是哪个文件

支持 px 向 vw 的单位转换

这个配置是直接从 xp 的项目拷贝过来的,即 postcss.config.js

module.exports = {
plugins: {
autoprefixer: {},
'postcss-px-to-viewport': {
viewportWidth: '375', // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
// propList: ['*', '!background-image'],
selectorBlackList: [
// 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
'ignore',
],
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位
mediaQuery: false, // 允许在媒体查询中转换`px`
// exclude: /(\/|\\)(node_modules)(\/|\\)/
},
},
}

但是这里需要安装两个东西

yarn add -D postcss-loader postcss-px-to-viewport

因为是对 css 做一个预处理,所以应该是在 sass-loader 转换为 css 之后应该执行这个 loader,所以直接在 sass-loader 后添加这个 loader 即可

提交自动检查 commit

pre-commit 之前要能够自动检查代码以及 commit,首先需要安装 husky

yarn add -D husky

然后在 package.json 中做配置

要规范 commit 提交,就必须安装如下的库

yarn add -D @commitlint/cli @commitlint/config-conventional

然后新建一个 commitlint.config.js,自定义 commit 提交规则

// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'chore',
'feat',
'fix',
'test',
'perf',
'style',
'merge',
'config',
'improvement',
],
],
},
}

这样每次提交时都会先检查代码,然后再检查提交,提交的规则可以参考

总结

以上基本就是搭建模板中踩过的坑以及一系列的搭建步骤了,总的来说,还是手撸一遍项目比较香,不仅比当初的 cra eject 项目简单,而且也更利于后续人员的优化和配置