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