webpack从0开始初始化搭建一个React17 + TS项目
快速初始化搭建一个react项目,可以使用React官方的create-react-app
,也可以使用一些封装好的脚手架比如阿里的飞冰ice
。但是这些方法想要进行定制化配置需要费一些周折(比如create-react-app
需要npm run eject
来吐出配置文件),而自己用webpack搭建一个简单的应用可以帮助我们更好地理解一个react项目是如何构建的。
项目文件树如下所示:
1. package.json
依赖项包括了webpack以及用到的一系列打包插件和loader,react相关库以及antd相关库
{
"name": "xxx",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config ./webpack.prod.config.js --mode production",
"dev": "webpack serve --hot --config ./webpack.config.js --mode development --port 8080",
"typeCheck": "tsc --watch --noEmit"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@types/dom-mediacapture-record": "^1.0.7",
"@types/offscreencanvas": "^2019.6.2",
"antd": "^4.16.3",
"duration-time-conversion": "^1.0.2",
"loadsh": "0.0.4",
"react": "^17.0.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"resize-observer-polyfill": "^1.5.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/preset-env": "^7.12.10",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@types/lodash": "^4.14.171",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/uuid": "^8.3.0",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.13.3",
"copy-webpack-plugin": "^7.0.0",
"core-js": "^3.8.1",
"css-loader": "^5.0.1",
"fast-sass-loader": "^1.5.0",
"html-webpack-plugin": "^5.0.0-alpha.15",
"less": "^3.13.0",
"less-loader": "^7.1.0",
"mini-css-extract-plugin": "^1.3.5",
"node-sass": "^5.0.0",
"style-loader": "^2.0.0",
"typescript": "^4.1.3",
"webpack": "^5.10.1",
"webpack-bundle-analyzer": "^4.2.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0",
"worker-loader": "^3.0.7"
}
}
上面的package.json
包含了基本的包和一些基本的命令
2. webpack.config.js
2.1 入口文件
入口文件设置为 src/index.tsx
// webpack.config.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root')
);
组件入口App.tsx
// App.tsx
import React from 'react';
import style from './app.less';
const App = () => {
return (
<div className={style.helloWorld}>HelloWorld</div>
);
};
export default App;
2.2 CDN引入依赖
react, react-dom 不打包进项目里,可以采用CDN的方式在index.html里引入:
// webpack.config.js
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
用cdn的方式引入React和ReactDOM
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="shortcut icon" type="image/x-icon" href="https://img.alicdn.com/tfs/TB1Mo_xl0Tfau8jSZFwXXX1mVXa-68-76.png" />
<title>xxx</title>
</head>
<body>
<div id="root"></div>
<script src="https://g.alicdn.com/code/lib/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://g.alicdn.com/code/lib/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
</body>
</html>
当然,为了在开发和协作中能够有比较好的体验,还是需要在package.json的dependencies里加上react和ReactDOM,以便在本地的node_modules里安装这两个包
2.3 TS解决方案
对于ts文件,和js一样统一交给babel-loader来处理(babel7 之后增加了对ts的支持,且比ts-loader更快)
{
test: /\.(ts|js)x?$/,
use: 'babel-loader',
exclude: /(node_modules)/,
}
2.4 样式解决方案
2.4.1 模块化解决方案
react的样式不像vue那样,直接写在<style scoped></style>
里加个scoped就能自动加上哈希值来实现模块化,而是需要手动选择一个模块化方案。常见的有CSS in JS
,CSS Modules
等方案。这里选择CSS Modules
。
本项目的样式文件都采样less编写,所以这里匹配less文件用css-loader
来配置CSS Modules:
CSS Modules 教程
{
test: /\.less$/,
exclude: [/node_modules/, /antd.dark.less$/], // 忽略antd
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[local]_[hash:base64:5]' },
},
},
{
loader: 'less-loader',
},
],
},
这样就可以在tsx文件里引入less文件并使用
import React from 'react';
import style from './app.less'; //引入less文件
const App = () => {
return (
<div className={style.helloWorld}>Hello World</div> // 把style当做对象来使用
);
};
export default App;
为了类型检查不报错,需要在src/types/global.d.ts
里指定less
// src/types/global.d.ts
declare module '*.less';
2.4.2 自定义antd的样式变量
注意如果需要修改antd
的一些全局变量的配置(如主题色等),需要手动引入antd.less
/antd.dark.less
等文件(而不是antd.css
),所以这里注意要在exclude
项里匹配这些文件,防止它们也被css-loader打上哈希标签;
然后单独开一条规则去处理antd的样式文件(我这里用的是antd的暗黑主题,所以是antd.dark.less
,正常主题应该是antd.less
)
{
test: /antd.dark.less$/, // 对antd的less文件制定特殊规则,不要应用css modules
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'less-loader',
options: {
lessOptions:{
modifyVars: {
'@primary-color': '#dc7f31'
},
javascriptEnabled: true,
}
}
},
],
},
在less-loader里,我加上了modifyVars去修改一些antd的全局配置,antd官网给出了常用的样式变量:
@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影
2.4.3 全局变量管理
全局变量管理采用css 的 var()函数。
在app.less里,可以定义一些全局的样式变量。
// app.less
:global { // :global表示不被css-loader打包时加上哈希值
html,
body,
* {
margin: 0;
padding: 0;
font-size: 14px;
box-sizing: border-box;
color: #d3d3d3;
}
:root {
--font-color-1: #d3d3d3;
--bc-main-1: #000;
--bc-main-2: #1b1b1b;
--bc-main-3: #333435;
--bc-main-4: #333333;
--theme-color: #dc7f31;
--bc-box: #1f1f1f;
}
}
- :global 表示不被css-loader打包时加上哈希值的标记
- :root是一个伪类,表示文档根元素,非IE及ie8及以上浏览器都支持,在:root中声明相当于
全局属性
,只要当前页面引用了:root segment所在文件,都可以使用var()来引用 - 变量需要以
--
开头
在其他样式文件里,就可以使用上述定义的变量
// example.less
div{
background-color: var(--bc-main-1);
}
综上,webpack.config.js是这样:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
const resolve = (...dir) => path.resolve(__dirname, ...dir);
module.exports = {
entry: {
app: path.resolve(__dirname, './src/index.tsx'),
},
output: {
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@': resolve('./src'),
},
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
devServer: {
disableHostCheck: true,
https: true, // 本地调试开启https
},
module: {
rules: [
{
test: /\.(ts|js)x?$/,
use: 'babel-loader',
exclude: /(node_modules)/,
},
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{
test: /antd.dark.less$/, // 对antd的less文件制定特殊规则,不要应用css modules
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'less-loader',
options: {
lessOptions:{
modifyVars: {
'@primary-color': '#dc7f31',
'@error-color': '#f5222d'
},
javascriptEnabled: true,
}
}
},
],
},
{
test: /\.less$/,
exclude: [/node_modules/, /antd.dark.less$/], // 忽略antd
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[local]_[hash:base64:5]' },
},
},
{
loader: 'less-loader',
},
],
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'fast-sass-loader'],
},
{
test: /\.worker\.js$/,
use: { loader: 'worker-loader', options: { inline: 'fallback' } },
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: resolve('./', 'index.html'),
filename: 'index.html',
favicon: false,
minify: {
removeComments: true,
collapseWhitespace: true,
},
}),
],
};