webpack + react + antd 项目框架搭建
本文参考:https://github.com/aweleey/webpack-antd-demo
在开发 webpack + react + adnt 构建的项目过程中,一直对整个项目架构是一知半解的,所以自己搭建一个项目框架,熟悉一下整个流程以及架构。并且记录下这个过程,便于自己以后查看。
说明
目录结构
webpack-react-demo
├── README.md // 本教程
├── package.json
├── pages // 放置页面, 业务页面代码
├── src
│ ├── index.html // 模板, HtmlWebpackPlugin插件会把相关资源注入后放入dist文件夹
│ ├── index.js // 项目入口
│ ├── app.js // 页面入口
│ ├── api // 请求的api
│ ├── assets // 资源文件夹
│ ├── bootstrap // 项目入口之前执行
│ │ ├── http-interceptors.js // 网络请求拦截器
│ │ └── index.js // bootstrap入口文件
│ ├── common
│ │ ├── constants.js // 用于存放静态变量
│ │ └── utils.js // 放置公共方法
│ ├── component // 自定义组件 , 例如 Loading 和 404
│ │ ├── Loading
│ │ │ └── index.js
│ │ └── NotFound
│ │ └── index.js
│ ├── I18N // 国际化
│ ├── pages // 业务页面代码
│ ├── router // 路由
│ │ └── index.js
│ └── store // 数据管理
│ ├── app.js
│ ├── index.js // 入口, 根据业务自行创建
│ └── ui.js
├── webpack.common.js // webpack 公共配置
└── webpack.config.js // webpack 开发配置
初始化项目
新建项目
mkdir webpack-react-demo && npm init -y
webpack
1. 安装 webpack
npm install webpack webpack-cli --save-dev
2. 配置文件
新建配置文件
touch webpack.config.js
webpack.config.js
const path = require('path');
module.exports = {
entry: {
main: './src/index.js' // 程序入口
},
output: {
path: path.resolve(__dirname, 'dist'), // 打包后文件输出目录
filename: 'bundle.js' // 输出文件名
}
}
3. 配置 npm 脚本(npm scripts)
新增命令 “dev”,这条命令指定配置文件为 webpack.config.js, 并将模式设置为 “development” (开发环境)。
package.json
"script": {
"dev": "webpack --config webpack.dev.js --mode development"
}
4. 执行命令编译文件
新建入口文件
mkdir src && touch ./src/index.js
index.js
执行打包命令
npm run dev
执行完成后,会在 /dist 目录下生成 bundle.js
5. 测试
新建 index.html
touch ./dist/index.html
index.html
<!DOCTYPE html>
<html lang="en">
<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">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./bundle.js"></script>
</body>
</html>
现在,用浏览器打开 index.html,可以看到 “hello webpack”
Babel
Babel 是一个用于将 ECMAScript 2015+ 代码转换为向后兼容版本的 Javascript 代码的工具链。使用 Babel 后我们无需担心新的语法无法兼容低版本浏览器问题。
Babel 其实是几个模块化的包,其核心功能位于称为 babel-core 的 npm 包中,webpack 可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析 ES6 的 babel-env-preset 包和解析 JSX 的 babel-preset-react 包)。
安装
npm install --save-dev babel-loader@8 @babel/core @babel/preset-env
babel-loader 是 webpack 的 loader,加载 ES2015+ 代码,然后使用 Babel 转换为 ES5。
@babel/core 调用 Babel 的 API 进行转码
@babel/preset-env 根据你支持的环境自动决定适合你的 Babel 插件的 Babel preset
Babel 配置文件
.babelrc
{
"presets": ["@babel/preset-env"],
"plugins": []
}
webpack.config.js
const path = require('path');
module.exports = {
entry: {
main: './src/index.js' // 程序入口
},
output: {
path: path.resolve(__dirname, 'dist'), // 打包后文件输出目录
filename: 'bundle.js' // 输出文件名
},
module: {
rules: [{
test: /\.js$/, // 正则匹配 js 文件
exclude: /node_modules/, // 排除 node_modules 文件夹
use: 'babel-loader' // 使用 babel-loader 解析 js 文件
}]
}
}
当我们使用 /.js$/ 来匹配 JS 文件时,位于 node_modules 中的 JS 文件也会被匹配到并被编译,为了防止这种情况,我们配置 exclude 选项,排除 node_modules 中的文件。
更多 rules 选项
修改一下 index.js,使用 ES6 语法。
index.js
const useBabel = () => {
const app = document.getElementById('app');
app.innerHTML = '<h1>using babel!</h1>';
}
useBabel();
现在,我们来打包一下文件 ‘npm run dev’,我们可以验证一下 Babel 是否做了转换,打开编译后的 bundle.js 可以看到被编译后的是 ES5 语法。
React
安装 React
npm install --save react react-dom
react-dom 是 react 中的部分功能拆分独立出来的包。
为了让 Webpack 正确执行 React 语法,我们还需要使用 @babel/preset-react 编译 React 语法。
安装 @babel/preset-react
npm inatsll @babel/preset-react --save-dev
.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": []
}
使用 React
index.js
import React from 'react';
import { render } from 'react-dom';
render(<div>Hello React</div>, document.getElementById('app'));
现在编译一下代码 ‘npm run dev’,打开浏览器,就可以看到效果了。
介绍一下 vscode 好用的 react 格式化插件
编写一个 hello 组件
新建文件 src/component/hello
cd src && mkdir component
cd component && mkdir hello
cd hello && touch index.js
hello/index.js
import React, {
Component
} from 'react';
export default class Hello extends Component {
render() {
return (
<div>
<h1> hello component</h1>
</div>
);
}
}
index.js
import React from "react";
import { render } from "react-dom";
import Hello from './component/hello';
render(<div>
<Hello></Hello>
</div>, document.getElementById("app"));
安装 antd
npm i antd --save
配置 antd 按需加载
配置了按需加载后,无需为模块单独引入样式。
npm i babel-plugin-import --save-dev
.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
]
]
}
加载 CSS
安装 webpack 加载 CSS 的工具:css-loader 和 style-loader
npm i css-loader style-loader --save-dev
webpack.config.js
const path = require('path');
module.exports = {
entry: {
main: './src/index.js' // 程序入口
},
output: {
path: path.resolve(__dirname, 'dist'), // 打包后文件输出目录
filename: 'bundle.js' // 输出文件名
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/, // 不解析 node_modules 文件夹
use: 'babel-loader'
}, {
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
}
}
使用 antd 样式
在 Hello 组件中使用 antd/Alert 组件
hello/index.js
import React, {
Component
} from 'react';
import { Alert } from 'antd';
export default class Hello extends Component {
render() {
return (<div>
<h1> hello component</h1>
<Alert message="Success Text" type="success" />
</div>
);
}
}
打包一下文件 ‘npm run dev’,现在可以看到 antd 的样式已经被引进来了。
html-webpack-plugin
目前我们的 bundle.js 是手动在 index.html 中引入的,现在我们要让 bundle.js 自动插入 index.html。需要用到 webpack 插件 html-webpack-plugin
安装
npm install html-webpack-plugin --save-dev
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js' // 程序入口
},
output: {
path: path.resolve(__dirname, 'dist'), // 打包后文件输出目录
filename: 'bundle.js' // 输出文件名
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}, {
test: /\.css$/,
use: ["style-loader", "css-loader"]
}]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 输出文件名
template: path.join(__dirname, 'src/index.html') // html 模板文件位置
})
]
}
在 src 目录下新建一个模板文件
src/index.html
<!DOCTYPE html>
<html lang="en">
<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">
<title>wenpack-react-demo</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
现在删除 dist 目录下所有文件,打包一下程序 ‘npm run dev’,可以看到 dist 目录下 index.html 中 bundle.js 被自动引入。
react-router
安装
npm install --save-dev react-router-dom
新建路由文件夹
cd src
mkdir router && touch ./router/index.js
component 下新建一个 bye 组件
mkdir bye && touch ./bye/index.js
bye/index.js
import React, { Component } from 'react';
import { Alert } from 'antd';
export default class TestRouter extends Component {
render() {
return (<div>
<h1>Bye Component</h1>
<Alert message="info Text" type="info" />
</div>)
}
}
src 下新建 router 文件夹
mkdir router && touch ./router/index.js
编写一个基本的路由
router/index.js
import React from 'react';
import { HashRouter as Router, Route, Link, Switch } from 'react-router-dom';
import Hello from '../component/hello';
import Bye from '../component/bye';
const Home = () => (
<div>
<h1>This is home page!</h1>
</div>
)
const getApp = () => (
<Router>
<div>
<ul>
<li><Link to='/hello'>home</Link></li>
<li><Link to='/bye'>bye</Link></li>
</ul>
<hr />
<Switch>
<Route path='/hello' component={ Hello } />
<Route path='/bye' component={ Bye } />
</Switch>
</div>
</Router>
)
export default getApp;
Link 是为了在页面上添加一个跳转的链接,Route 将路径与组件对应起来。
此时,我们的路由使用的是 HashRouter,是通过在路径后加 ‘#’,来区分不同的页面,这样做有点不太美观,不过,它能兼容老版本浏览器。如果不想要路径中的 ‘#’,我们需要使用 BrowerRouter。
同时,我们需要使用 webpack-dev-server 提供一个本地服务器。
webpack-dev-server
webpack-dev-server 允许我们在不重新打包文件的情况下,自动监听我们的代码改动并实时编译刷新。
安装
npm install --save-dev webpack-dev-server
修改配置文件
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
devServer: {
contentBase: path.join(__dirname, 'dist'), // 监听位置
port: 9000, // 设置服务器端口
open: true,
historyApiFallback: true // 将所有 404 都返回 index.html
}
}
假如这里没有配置 “historyApiFallback: true”,那么当我们在浏览器地址栏直接输入某个页面 url 后,会返回 “404 not found”。
这篇 浅谈前后端路由与前后端渲染 很详细地介绍了原因及解决办法。
我们使用 webpack-dev-server 方便我们前端代码调试,但是如果需要处理路由请求,我们必须要拥有一个属于我们自己的服务器,服务器搭建方法见 这里。
code-splitting
在一个较大的项目里,加载完所有常常需要很长的时间,但我们并不需要一次性加载完所有的组件,而更希望在我们用到的时候再去加载它,这就是 code-splitting。
在我们的 React 项目中,我们可以使用 React Loadable 来实现按需加载。
webpack 的 dynamic import 实现主要利用 ECMAScript 的 import() 动态加载特性,而 import() 目前只是一个草案,如果需要使用此方法,需要引入对应 转换器 “babel-plugin-syntax-dynamic-import”
安装
npm i react-loadable --save
npm install babel-plugin-syntax-dynamic-import --save-dev
.babelrc
{
// ...
"plugins": [
// ...
"syntax-dynamic-import"
]
}
我们修改一下 router/index.js,使用 import() 来代替 import
router/index.js
import React from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
// 删除原来的 import 引入组件方式
// import Hello from '../component/hello';
// import Bye from '../component/bye';
// 定义一个简单的 Loading 状态组件
function MyLoadingComponent() {
return <div>Loading...</div>;
}
// 使用 Loadable、import() 来动态引入组件
const Hello = Loadable({
loader: () => import('../component/hello'),
loading: MyLoadingComponent
})
const Bye = Loadable({
loader: () => import('../component/bye'),
loading: MyLoadingComponent
})
const getApp = () => (
<Router>
<div>
<ul>
<li><Link to='/hello'>home</Link></li>
<li><Link to='/bye'>bye</Link></li>
</ul>
<hr />
<Switch>
<Route path='/hello' component={ Hello } />
<Route path='/bye' component={ Bye } />
</Switch>
</div>
</Router>
)
export default getApp;
现在,只有当路由匹配时,才会引入相应的组件的 js 文件,达到了 code-splitting 的效果。
fetch
我们还需要发送网络请求,这里我们选用 fetch。
安装
npm i isomorphic-fetch --save
isomorphic-fetch 的优点是既能兼容浏览器端,又能兼容 node 端。
引入 isomorphic-fetch 后会提供一个全局的 fentch 方法。
解析 async 语法
我们来封装一个网络请求的方法
request.js
require('isomorphic-fetch');
const site = '127.0.0.1';
module.exports = ({
url = '/',
port = 3030,
method = 'get'
}) => {
console.log('requuest!!');
return fetch(`http://${site}:${port}${url}`, {
method: method.toLowerCase(),
headers: {
'Content-Type': 'application/json',
},
credentials: "same-origin"
}).then(res => {
if (res.ok) {
return res.json()
}
}).then(data => {
return data;
})
}
现在来通过 request 方法发送一次请求。
方法如下:
async getInfo() {
const data = await request({url: '/api/about'});
return data;
}
此时会报错 ‘Uncaught ReferenceError: regeneratorRuntime is not defined’,是因为缺少解析 ES6 generator 语法功能
我们需要使用 @babel/plugin-transform-runtime 插件来转换 async 语法
npm install --save-dev @babel/plugin-transform-runtime
@babel/runtime 提供产品环境依赖
npm install --save @babel/runtime