Skip to content

ariesly15/webpack-antd-demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React框架入门

说明

项目托管于GitHub, 文档很多, 需要有耐心

1.本教程基于mac环境

node -v
# v6.9.5
npm -v
# 3.10.10

2.每个命令行块都是以根目录为基础的。例如下面命令行块,都是基于根目录的。

cd src/pages
mkdir Home

3.所用的库的版本(2018年09月)是最新的

webpack-antd-demo
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]

4.目录说明

webpack-antd-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.dev.js                  // webpack 开发配置
└── webpack.prod.js                 // webpack 线上配置

初始化项目

mkdir webpack-antd-demo && cd webpack-antd-demo
npm init
# 按照提示填写基本信息或者用 npm init -y 一步创建完成

webpack

1.安装webpack

npm i webpack webpack-cli --save-dev
# --save        线上依赖
# --save-dev    开发依赖

2.根据webpack文档编写最基础的配置文件

touch webpack.dev.js
// webpack.dev.js 内容
const path = require('path');

module.exports = {
    /*入口 (webpacak@4可以不写, 默认读取./src/index.js)*/
    entry: path.join(__dirname, 'src/index.js'),
    
    /*输出到dist文件夹,输出文件名字为bundle.js*/
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js'
    }
}

3.创建入口文件并写入内容

mkdir src && touch  ./src/index.js
//  ./src/index.js 内容
document.getElementById('app').innerHTML = "webpack-antd-demo"

4.配置 NPM 脚本(NPM Scripts)

// 在package.json中的scrripts下添加
"dev": "webpack --config webpack.dev.js --mode development"

// 结果入下
{
  "name": "webpack-antd-demo",
  "version": "1.0.0",
  "description": "## 说明",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --config webpack.dev.js --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.17.1",
    "webpack-cli": "^3.1.0"
  }
}

5.执行命令编译文件

npm run dev

执行后查看dist目录是否用bundle.js文件

6.测试

# 在dist目录创建html文件
touch ./dist/index.html
<!-- dist/index.html内容 -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webpack-ant-demo</title>
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript" src="./bundle.js"></script>
</body>
</html>

用浏览器打开dist/index.html, 可以看到webpack-antd-demo


babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。 通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。

1.安装

babel-core 调用Babel的API进行转码 babel-loader 使用Babel和webpack转换JavaScript文件 webpack中babel-loader用法 babel-preset-env 相关介绍

# 安装
npm i --save-dev babel-loader@8 @babel/core @babel/preset-env

2.配置

创建babel的配置文件.babelrc

touch .babelrc
// .babelrc 内容

{
    "presets": ["@babel/preset-env"],
    "plugins": []
}
// 修改webpack.dev.js,增加babel-loader!

module: {
    rules: [{
        test: /\.js$/,  // 正则匹配以 .js 结尾的文件来使用 babel 解析
        use: ['babel-loader?cacheDirectory=true'],  // cacheDirectory是用来缓存编译结果,下次编译加速
        include: path.join(__dirname, 'src')    // 需要解析的目录
    }]
}
// 修改 ./src/index.js 使用es6的箭头函数

const useBabel = text => document.getElementById('app').innerHTML = text
useBabel('正在使用babel')

执行打包命令 npm run dev 浏览器打开 dist/index.html 查看效果


React

安装react

npm i --save react react-dom

修改./src/index.js文件, 内容如下

import React from 'react'
import {render} from 'react-dom'

render(
    <div>Hello React !!!</div>,
    document.getElementById('app')
)

为了能让webpack正确执行, 还需要安装 @babel/preset-react 来正确编译 React 代码, 并修改.babelrc文件

npm i @babel/preset-react --save-dev
// 修改后的内容
{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": []
}

执行打包命令 npm run dev 浏览器打开 dist/index.html 查看效果


简单做下改进,把Hello React放到组件里面。体现组件化~

cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch index.js

按照React语法,写一个Hello组件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return <div>
            我是独立的 Hello 组件
        </div>
    }
}

然后让我们修改src/index.js,引用Hello组件!

// 修改 ./src/index.js 为如下内容

import React from 'react'
import {render} from 'react-dom'
import Hello from './component/Hello'

render(
    <Hello/>,
    document.getElementById('app')
)

执行打包命令 npm run dev 浏览器打开 dist/index.html 查看效果


Antd

# 安装antd
npm i antd --save

配置Antd的按需加载

使用 babel-plugin-import

npm i babel-plugin-import --save-dev
// 修改 .babelrc 文件, 修改后的文件内容如下
{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": [
        ["import",{
            "libraryName": "antd",
            "style": "css"
        }]
    ]
}

然后只需从 antd 引入模块即可,无需单独引入样式。等同于下面手动引入的方式。

// babel-plugin-import 会帮助你加载 JS 和 CSS
import { Alert } from 'antd';

之后安装编译css的插件

css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能; style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中

npm i css-loader style-loader --save-dev
// 修改 webpack.dev.js, 修改后内容如下
const path = require("path");

module.exports = {
    entry: path.join(__dirname, "src/index.js"),
    output: {
        path: path.join(__dirname, "./dist"),
        filename: "bundle.js"
    },
    module: {
        rules: [
            {
                test: /\.js$/, // 正则匹配以 .js 结尾的文件来使用 babel 解析
                use: ["babel-loader?cacheDirectory=true"], // cacheDirectory是用来缓存编译结果,下次编译加速
                include: path.join(__dirname, "src") // 需要解析的目录
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
             }
        ]
    }
};

准备工作做好后接下来修改一下 Hello 组件来测试一下 Antd

// 修改后的 Hello/index.js 内容如下
import React, {Component} from 'react';
import { Alert } from "antd";

export default class Hello extends Component {
    render() {
        return <div>
            我是独立的 Hello 组件
            <Alert message="Success Text" type="success" />
            <Alert message="Info Text" type="info" />
            <Alert message="Warning Text" type="warning" />
            <Alert message="Error Text" type="error" />
        </div>
    }
}

执行打包命令 npm run dev 浏览器打开 dist/index.html 查看效果


HtmlWebpackPlugin

自动把js插入到你的模板index.html里面去 GitHub 印记中文

npm i html-webpack-plugin --save-dev

创建模板html

cd src && touch index.html
<!-- ./src/index.html 内容如下 -->

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>webpack-antd-demo</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

修改 webpack.dev.js 文件, 修改后的内容如下

const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    output: {
        path: path.join(__dirname, "./dist"),
        filename: "bundle.js"
    },
    module: {
        rules: [
            {
                test: /\.js$/, // 正则匹配以 .js 结尾的文件来使用 babel 解析
                use: ["babel-loader?cacheDirectory=true"], // cacheDirectory是用来缓存编译结果,下次编译加速
                include: path.join(__dirname, "src") // 需要解析的目录
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
             }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        })
    ]
};

react-router

地址 | 中文

# 安装
npm i --save react-router-dom

# 新建router文件夹和组件
cd src
mkdir router && touch router/index.js

src/commponent/Hello 目录下创建 TestAntd.jsTestRouter.js 组件(此为测试路由创建, 正常情况下最好是每个文件夹是一个组件)

// TestAntd.js
import React, {Component} from 'react';
import { Alert } from "antd";

export default class TestAntd extends Component {
    render() {
        return <div>
            <Alert message="我是独立的 TestAntd 组件" type="success" />
        </div>
    }
}

// TestRouter.js
import React, {Component} from 'react';
import { Alert } from "antd";

export default class TestRouter extends Component {
    render() {
        return <div>
            <Alert message="我是独立的 TestRouter 组件" type="info" />
        </div>
    }
}

按照 react-router 文档添加一个基本的router

// router/index.js 内容
import React from 'react'
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom'

import Hello from '../component/Hello'
import TestAntd from '../component/Hello/TestAntd'
import TestRouter from '../component/Hello/TestRouter'

const getRouter = () => <Router>
    <div>
        <ul>
            <li><Link to="/">首页</Link></li>
            <li><Link to="/router">TBestRouter</Link></li>
            <li><Link to="/antd">TestAntd</Link></li>
        </ul>
        <Switch>
            <Route exact path="/" component={Hello} />
            <Route exact path="/antd" component={TestAntd}/>
            <Route exact path="/router" component={TestRouter}/>
        </Switch>
    </div>
</Router>

export default getRouter

执行打包命令 npm run dev 浏览器打开 dist/index.html 查看效果

那么问题来了~我们发现点击首页``TestRouter``TestAntd没有反应。不要惊慌,这是正常的。因为我们 router/index.js 中使用的是 BrowserRouter, BrowserRouter 需要一个简单的web服务器, 有如下两种方法实现

  • Nginx, Apache, IIS等配置启动一个简单的的WEB服务器。
  • 使用webpack-dev-server来配置启动WEB服务器。

或者我们先简单的修改一下 router/index.js 代码, 把 BrowserRouter 改为 HashRouter 即可立刻查看效果

// 把
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom'
// 修改为
import {HashRouter as Router, Route, Switch, Link} from 'react-router-dom'

webpack-dev-server

GitHub | 中文文档

# 安装
npm i webpack-dev-server --save-dev

修改webpack.dev.js文件

module.exports = {
    //...
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        port: 9090
    }
}

devServer中的配置根据自身需求自行参照文档添加

修改 package.json 文件, 在scripts中添加

{
    // ...
    "scripts": {
        // ...
        "server": "webpack-dev-server --config webpack.dev.js --mode development"
    },
    // ...
}

historyApiFallback 任意的404响应都被替代为index.html。 有什么用呢?你现在运行npm run server,然后打开浏览器,访问http://localhost:9090 ,然后点击TestRouter到链接http://localhost:9090/router 然后刷新页面试试。是不是发现刷新后404了。为什么?dist文件夹里面并没有router.html,当然会404了,所以我们需要配置 historyApiFallback ,让所有的404定位到index.html

建议把 router/index.js 中的 HashRouter 改为 BrowserRouter

执行 npm run server 后, 浏览器打开 http://localhost:9090 查看效果


编译图片

npm i --save-dev url-loader file-loader
// 修改webpack.dev.js, 添加规则(放在rules下)

module.exports = {
    // ...
    module: {
        rules: [
            // ...
            {
                test: /\.(png|jpg|gif)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        // olimit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求
                        limit: 8192
                    }
                }]
            }
        ]
    },
    // ...  
}
# 创建图片目录
mkdir -p src/assets/images
// 修改 `Hello/index.js` 文件添加如下代码(图片文件在源码中获取)
<div>
    <div>6kb</div>
    <img src={require('../../assets/images/antd6kb.png')} />
    <div>10kb</div>
    <img src={require('../../assets/images/antd10kb.png')} />
</div>

重新执行 npm run server 后, 浏览器打开 http://localhost:9090 , 在 开发者工具 中查看

按需加载 code-splitting

参考链接: GitHub | React Loadable 简介 | react-loadable原理浅析

# 安装 react-loadable
npm i react-loadable --save
# 安装 babel-plugin-syntax-dynamic-import 来支持react-loadable的import方法
npm i babel-plugin-syntax-dynamic-import --save-dev

修改 .babelrc

{
    // ...
    "plugins": [
        // ...
        "syntax-dynamic-import"
    ]
}

先准备一个 Loding 组件, 根据实际情况自行改写

// Loading 组件路径 src/component/Loading/index.js
import React, { Component } from "react";

export default class Loading extends Component {
    render() {
        const { isLoading, error } = this.props;

        // Handle the loading state
        if (isLoading) {
            return <div>Loading...</div>
        }
        // Handle the error state
        else if (error) {
            return <div>
                Sorry, there was a problem loading the page.
                <div>{JSON.stringify(error, null, 4)}</div>
            </div>
        } else {
            return null
        }
    }
}

修改 router/index.js 文件

// ...
import Loadable from 'react-loadable'
import Loading from '../component/Loading'
// ...
/**
此处为删除项
import TestAntd from '../component/Hello/TestAntd'
import TestRouter from '../component/Hello/TestRouter'
 */
const TestAntd = Loadable({
    loader: () => import('../component/Hello/TestAntd'),
    loading: Loading
})
const TestRouter = Loadable({
    loader: () => import('../component/Hello/TestRouter'),
    loading: Loading
})

只有路由匹配的时候,组件才被import进来,达到了code splitting的效果,也就是我们常说的按需加载,�代码分块,而不是一开始就将全部组件加载。 如上图所示, 点击不同的路由都会加载一个chunk.js, 代码中除了首页之外只写了两个按需加载的组件 TestAntdTestRouter, 分别对应上图 1.bundle.js0.bundle.js (对应关系根据实际开发打包而定, 不必关注)

重新执行 npm run server 后, 浏览器打开 http://localhost:9090 查看效果


让组件支持静态属性 (static)

npm i @babel/plugin-proposal-class-properties --save-dev

修改 .babelrc 文件

{
    // ...
    "plugins": [
        // ...
        "@babel/plugin-proposal-class-properties"
    ]
}

修改 Hello/TestAntd.js 文件来测试是否配置正确

import React, {Component} from 'react';
import { Alert } from "antd";

export default class TestAntd extends Component {

    static defaultProps = {
        value: 'test static'
    }

    render() {
        const {value} = this.props

        return <div>
            <Alert message="我是独立的 TestAntd 组件" type="success" />
            <div>{value}</div>
        </div>
    }
}

重新执行 npm run server 后, 浏览器打开 http://localhost:9090 查看效果


抽取公共代码

vendor.jpg

可以看到 bundle.js 文件有 3MB, 因为此时 bundle.js 中包含了 React, React-dom, React-Router 等公共库文件, 这些代码基本上不会改变的。但是,他们合并在bundle.js里面,每次项目发布,重新请求bundle.js的时候,相当于重新请求了react等这些公共库。浪费资源

修改 webpack.dev.js

module.exports = {
    // ...
    optimization: {
        runtimeChunk: 'single'
    }
}

重新执行 npm run server 后可以看到公共库已被抽取 vendor-extract.jpg


缓存

文档地址

想象一下这个场景

我们网站上线了,用户第一次访问首页,下载了 bundle.js ,第二次访问又下载了 bundle.js 这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js后,第二次就不下载了。 有一天,我们更新了home.js,但是用户不知道呀,用户还是使用本地旧的 bundle.js 怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫bundle.a.js ,第二次叫 bundle.b.js

修改 webpack.dev.js

module.exports = {
    output: {
        // ...
        filename: "[name].[chunkhash].js"
    },
    // ...
    plugins: [
        // ...
        new webpack.HashedModuleIdsPlugin()
    ],
    // ...
    optimization: {
        runtimeChunk: "single",
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    chunks: "all"
                }
            }
        }
    }
}

配置之后的,不管再添加任何新的本地依赖,对于每次构建,vendor hash 都会保持一致 文档


生产坏境构建

文档地址

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

touch webpack.prd.js
// webpack.prod.js

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    mode: 'production',
    output: {
        path: path.join(__dirname, "./dist"),
        filename: "[name].[chunkhash].js"
    },
    module: {
        rules: [
            {
                test: /\.js$/, // 正则匹配以 .js 结尾的文件来使用 babel 解析
                use: ["babel-loader?cacheDirectory=true"], // cacheDirectory是用来缓存编译结果,下次编译加速
                include: path.join(__dirname, "src") // 需要解析的目录
            },
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            // olimit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求
                            limit: 8192
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: path.join(__dirname, "src/index.html")
        }),
        new webpack.HashedModuleIdsPlugin()
    ],
    optimization: {
        runtimeChunk: "single",
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    chunks: "all"
                }
            }
        }
    }
};
// 修改package.json, 添加scripts
{
  // ...
  "scripts": {
    // ...
    "prod": "webpack --config webpack.prod.js --color --progress"
  },
  // ...
}

优化webpack配置

webpack.dev.jswebpack.prod.js 中有很多相同的代码, 每次修改都要顾及到两个文件, 用 webpack-merge 优化一下

# 创建公共配置
touch webpack.common.js

# 安装 webpack-merge
npm i webpack-merge --save-dev
// 修改 package.json文件, 修改后的 scripts 如下
{
  // ...
  "scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    // 由于目前webpack.prod.js和webpack.common.js只是相差了一个 mode, 所以修改为如下命令, 后续添加less是统一抽离
    "prod": "webpack --config webpack.common.js --mode production --color --progress"
  },
  // ...
}
// 抽取公共配置到 webpack.common.js 中
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    output: {
        path: path.join(__dirname, "./dist"),
        filename: "[name].[chunkhash].js"
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ["babel-loader?cacheDirectory=true"],
                include: path.join(__dirname, "src")
            },
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            limit: 8192
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: path.join(__dirname, "src/index.html")
        }),
        new webpack.HashedModuleIdsPlugin()
    ],
    optimization: {
        runtimeChunk: "single",
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    chunks: "all"
                }
            }
        }
    }
};

// webpack.dev.js 修改如下
const path = require("path");
const CommonConfig = require("./webpack.common");
const WebpackMerge = require("webpack-merge");

const DevConfig = {
    devtool: "inline-source-map",
    mode: "development",
    devServer: {
        contentBase: path.join(__dirname, "dist"),
        historyApiFallback: true,
        //  指定使用一个 host。默认是 localhost。如果你希望服务器外部可访问,写法如下
        host: "0.0.0.0",
        port: 9090
    }
};

module.exports = WebpackMerge(CommonConfig, DevConfig);

打包优化

每次打包前自动清理下dist文件。

npm i clean-webpack-plugin --save-dev
// webpack.common.js
const CleanWebpackPlugin = require('clean-webpack-plugin');

plugins: [
    new CleanWebpackPlugin(['dist'])
]

公共路径(public path)

文档

Store [mobx]

文档

# 安装
npm i mobx mobx-react --save
# 使用mobx开发, 需要启用decorators装饰器
npm i @babel/plugin-proposal-decorators --save-dev
# 创建 store 目录
cd src && mkdir store && cd store
touch index.js app.js
// 修改 .balelrc 文件
{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": [
        ["@babel/plugin-proposal-decorators", {"legacy": true}],
        ["import",{
            "libraryName": "antd",
            "style": "css"
        }],
        "syntax-dynamic-import",
        ["@babel/plugin-proposal-class-properties", {"loose" : true}]
    ]
}
// store/index.js
import AppStore from './app'

export const appStore = new AppStore()

// store/app.js
import {observable, action} from 'mobx'

export default class App {
    @observable
    count = 0

    @action
    updateCount(num){
        this.count = this.count + num
    }
}

// src/index.js
import React from 'react'
import {render} from 'react-dom'
import getRouter from './router'
import * as stores from './store'
import {Provider} from 'mobx-react'

render(<Provider {...stores}>
    {getRouter()}
</Provider>,document.getElementById('app'))

接下来测试一下, 修改 TestAntd/index.jsTestRouter/index.js

// TestAntd/index.js
import React, {Component} from 'react';
import { Alert } from "antd";
import {observer, inject} from 'mobx-react'

@inject('appStore')
@observer
export default class TestAntd extends Component {

    static defaultProps = {
        value: 'test static'
    }

    render() {
        const {value, appStore} = this.props

        return <div>
            <div>count: {appStore.count}</div>
            <button onClick={() => appStore.updateCount(1)}>++</button>
            <button onClick={() => appStore.updateCount(-1)}>--</button>

            <Alert message="我是独立的 TestAntd 组件" type="success" />
            <div>{value}</div>
        </div>
    }
}

// TestRouter/index.js
import React, {Component} from 'react';
import { Alert } from "antd";
import {observer, inject} from 'mobx-react'

@inject('appStore')
@observer
export default class TestRouter extends Component {
    render() {
        const {appStore} = this.props
        return <div>
            <div>count: {appStore.count}</div>
            <button onClick={() => appStore.updateCount(1)}>++</button>
            <button onClick={() => appStore.updateCount(-1)}>--</button>
            <Alert message="我是独立的 TestRouter 组件" type="info" />
        </div>
    }
}

跳转到 /antd 点击 ++ 或者 --, 后在跳转到 /router查看count是否和之前一样, 自行测试看效果


网络请求 (axios)

GitHub | 中文

安装请求库并创建一些测试代码, 涉及到的代码有点多, 耐心看完

关于polyfill Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。 举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

npm i axios @babel/polyfill --save
cd src && mkdir bootstrap api common
touch bootstrap/index.js
touch bootstrap/http-interceptors.js
touch api/test.js
touch store/ui.js
// src/index.js 添加两行代码
import './bootstrap'
import "@babel/polyfill";
// api/test.js
/**
 * {loading: true} 用于监控全局请求个数, 处理过程在 http-interceptors.js 中
 */
import axios from 'axios'

export default {
    testGet(){
        return axios.get('/api/testGet', {loading: true})
    },
    testPost(params){
        return axios.post('/api/testPost', params, {loading: true})
    },
    testDelete(params){
        return axios.delete('/api/testDelete', {data: params})
    }
}
// bootstrap/index.js  render之前进行注入的一些代码, 例如请求拦截器
import './http-interceptors'
// bootstrap/http-interceptors.js 相关错误处理和业务逻辑自行补全
import axios from 'axios'
import {uiStore} from '../store'

axios.defaults.baseURL = "/"
// token 验证, 需要的话自行打开注释
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;

// 添加请求拦截器
axios.interceptors.request.use(config => {
    console.log('%c request config', 'font-size:21pt;color:green', config)
    // 发送请求之前做的事情
    if(config.loading === true){
        uiStore.updateReqCount(+config.loading)
    }
    return config
}, error => {
    console.log('%c request error', 'font-size:21pt;color:red', error)
    // 对请求错误处理
    uiStore.updateReqCount(-1)
    return Promise.reject(error)
})

/**
 * 后端返回的数据格式
 * {
 *   code: 0,
 *   msg: '这是一条成功的消息, code为0, 其他code根据需求自定义',
 *   data: {...}
 * }
 */
// 添加响应拦截器
axios.interceptors.response.use(res => {
    console.log('%c response res', 'font-size:21pt;color:blue', res)
    // 对响应数据处理
    if(res && res.config && res.config.loading === true){
        uiStore.updateReqCount(-1)
    }
    const result = res.data
    if(result.hasOwnProperty('code') && result.code !== 0){
        // 根据需求自定义错误码, 统一处理
    }
    return result.data
}, error => {
    console.log('%c response error', 'font-size:21pt;color:red', error)
    // 对响应错误处理
    uiStore.updateReqCount(-1)
    return Promise.reject(error)
})

const handleError = (error) => {
    if(error instanceof Error){
        console.log('[ERROR]:', error)
    }
}
/**
  * store/ui.js
  * UI Store中常见存储的信息有:
  * Session 信息
  * 不会再后端存储的信息
  * 会全局影响UI的信息:
  *   Window尺寸
  *   提示消息
  *   当前语言
  *   当前主题
  * 更多可能存储的组件信息:
  *   当前选择
  *   工具条显示隐藏状态
  */

import {observable, action} from 'mobx'

export default class UiStore {
    // 表示在一时间段内请求的个数, 可用做全局 loading
    @observable reqCount = 0

    @action
    updateReqCount(num = 0){
        this.reqCount = this.reqCount + num
    }
}

Hello/testApi.js 创建测试请求的组件, 项目运行后, 分别点击 testGet, testPost, testDelete 查看 reqCount,变化

import React, {Component} from 'react';
import { Alert, Button } from "antd";
import {observer} from 'mobx-react'
import testApi from '../../api/test'
import {uiStore} from '../../store'
import './index.css'

@observer
export default class TestApi extends Component {

    async testGet(){
        const result = await testApi.testGet()
        console.log('TestApi testGet result:', result)
    }

    async testPost(){
        const result = await testApi.testPost({})
        console.log('TestApi testGet result:', result)
    }

    async testDelete(){
        const result = await testApi.testDelete({})
        console.log('TestApi testGet result:', result)
    }

    render() {
        return <div className="container">
            <div>
                <p>reqCount: {uiStore.reqCount}</p>
                <Button type="primary" onClick={() => this.testGet()}>testGet</Button>
                <Button onClick={() => this.testPost()}>testPost</Button>
                <Button onClick={() => this.testDelete()}>testDelete</Button>
            </div>
        </div>
    }
}

想要请求非本地的接口, 需要添加代理, 具体用法点这里, 接下来修改 webpack.dev.js

// easy-mock.com 免费的造假数据的网站
const ProxyUrl = "https://easy-mock.com/mock/5b8c9f2fdcc57313cd5b6678"

module.exports = WebpackMerge(CommonConfig, {
    // ...
    devServer: {
        // ...
        proxy: {
            "/api/*": {
                target: ProxyUrl,
                changeOrigin: true,
                secure: false
            }
        }
    }
});
// store/index.js
import UiStore from './ui'
export const uiStore = new UiStore()

修改路由组件, 把刚才创建的 testApi 组件添加到路由里

// router/index.js
const TestApi = Loadable({
    loader: () => import('../component/Hello/TestApi'),
    loading: Loading
})

<li><Link to="/testapi">TestApi</Link></li>

<Route exact path="/testapi" component={TestApi}/>

Page Not Found (404)

创建404页面

mkdir src/component/NotFound
touch src/component/NotFound/index.js
// NotFount/index.js
import React, {PureComponent} from 'react'

export default class NotFound extends PureComponent {
    render(){
        return <div style={{color: 'red', fontSize: 88}}>404</div>
    }
}

修改路由组件, 把刚才创建的 NotFount 组件添加到路由最下方

// router/index.js
<Switch>
    // ...
    <Route component={NotFound}/>
</Switch>

Less

npm i mini-css-extract-plugin less less-loader --save-dev

mini-css-extract-plugin | 翻译

mini-css-extract-plugin 插件用于将css提取到单独的文件。为每个包含css的JS文件创建一个css文件。支持css按需加载。

修改package中的scripts

{
  // ...
  "scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js --color --progress",
    "build": "webpack --config webpack.prod.js --color --progress"
  }
  // ...
}

重新整理webpack配置文件

修改后的webpack.common.js

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');

// 如果预先定义过环境变量,就将其赋值给`ASSET_PATH`变量,否则赋值为根目录
const ASSET_PATH = process.env.ASSET_PATH || '/';

module.exports = {
    output: {
        path: path.join(__dirname, "./dist"),
        filename: "[name].[chunkhash].js",
        publicPath: ASSET_PATH
    },
    module: {
        rules: [
            {
                test: /\.js$/, // 正则匹配以 .js 结尾的文件来使用 babel 解析
                use: ["babel-loader?cacheDirectory=true"], // cacheDirectory是用来缓存编译结果,下次编译加速
                include: path.join(__dirname, "src") // 需要解析的目录
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            // olimit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求
                            limit: 8192
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            filename: "index.html",
            template: path.join(__dirname, "src/index.html")
        }),
        new webpack.HashedModuleIdsPlugin()
    ],
    optimization: {
        runtimeChunk: "single",
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    chunks: "all"
                }
            }
        }
    }
};

修改后的webpack.dev.js

const path = require("path");
const WebpackMerge = require("webpack-merge");
const CommonConfig = require("./webpack.common");

const ProxyUrl = "https://easy-mock.com/mock/5b8c9f2fdcc57313cd5b6678";

module.exports = WebpackMerge(CommonConfig, {
    devtool: "inline-source-map",
    mode: "development",
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ["style-loader", "css-loader"]
            },
            {
                test: /\.less$/,
                use: ["style-loader", "css-loader", "less-loader"]
            }
        ]
    },
    devServer: {
        contentBase: path.join(__dirname, "dist"),
        historyApiFallback: true,
        //  指定使用一个 host。默认是 localhost。如果你希望服务器外部可访问,写法如下
        host: "0.0.0.0",
        port: 9090,
        proxy: {
            "/api/*": {
                target: ProxyUrl,
                changeOrigin: true,
                secure: false
            }
        }
    }
});

修改后的webpack.prod.js

const WebpackMerge = require("webpack-merge");
const CommonConfig = require('./webpack.common');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = WebpackMerge(CommonConfig, {
    mode: 'production',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, "css-loader"]
            },
            {
                test: /\.less$/,
                use: [MiniCssExtractPlugin.loader, "css-loader", 'less-loader']
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].css",
            chunkFilename: "[id].css"
        })
    ]
})

Scss

# 因为sass-loader依赖于node-sass,所以需要先安装node-sass
npm i sass node-sass --save-dev
npm i sass-loader --save-dev

sass-loader

webpack.dev.js 中添加

{
    test: /\.s(a|c)ss$/,
    use: ["style-loader","css-loader","sass-loader"]
}

webpack.prod.js 中添加

{
    test: /\.s(a|c)ss$/,
    use: [MiniCssExtractPlugin.loader,"css-loader","sass-loader"]
}

整理项目目录

文章开头有详细的目录说明

cd src
mkdir pages
cd common && touch utils.js contants.js

i18n (react-intl) 推荐

GitHub | 语法指南

npm i react-intl intl -save
// 创建相关文件
cd src && mkdir I18N && touch app.js
cd I18N && touch en_US.js zh_CN.js

为了兼容Safari各个版本,需要同时安装 intl,intl在大部分的『现代』浏览器中是默认自带的,但是Safari和IE11以下的版本就没有

//  src/app.js
import React, { Component } from "react";
import getRouter from "./router";
import { IntlProvider, addLocaleData } from "react-intl";
import { uiStore } from "./store";
import { observer } from "mobx-react";
import zh from 'react-intl/locale-data/zh'
import en from 'react-intl/locale-data/en'
addLocaleData([...en, ...zh])

@observer
export default class App extends Component {
    getLocale() {
        let result = {};
        switch (uiStore.language) {
            case "zh":
                result = require("./I18N/zh_CN");
                break;
            case "en":
                result = require("./I18N/en_US");
                break;
            default:
                result = require("./I18N/zh_CN");
        }
        return result.default || result;
    }

    render() {
        console.log('uiStore.language:', uiStore.language)
        const messages = this.getLocale()

        return (
            <IntlProvider locale={uiStore.language} messages={messages}>
                {getRouter()}
            </IntlProvider>
        );
    }
}

修改 src/index.js src-index.jpg

// src/I18N/zh_CN.js
export default {
    hello: '这是一段放在国际化文件中的中文.',
    withParams: '这是一段带有参数的国际化文案, { param }'
}

// src/I18N/en_US.js
export default {
    hello: 'This is a paragraph in English.',
    withParams: 'This is an international copy with parameters, {param}'
}

store/ui.js 中添加如下代码

@observable language = 'zh'

@action
updateLanguage(lang){
    this.language = lang
}

创建测试代码

// Hello/TestI18N.js 内容如下
import React, { Component } from "react";
import { FormattedMessage, FormattedDate, FormattedTime } from "react-intl";
import { uiStore } from "../../store";
import { Button } from "antd";

export default class TestI18N extends Component {
    render() {
        return (
            <div>
                <Button onClick={() => uiStore.updateLanguage('en')}>to en</Button>
                <Button onClick={() => uiStore.updateLanguage('zh')}>to zh</Button>
                <div>
                    <FormattedMessage id="hello" />
                </div>
                <div>
                    <FormattedMessage
                        id="withParams"
                        values={{ param: "[我是param]" }}
                    />
                </div>
                <div>
                    <FormattedDate value={Date.now()}/>
                </div>
                <div>
                    <FormattedTime value={Date.now()}/>
                </div>
            </div>
        );
    }
}

router/index.js 中添加如下代码

const TestI18N = Loadable({
    loader: () => import('../component/Hello/TestI18N'),
    loading: Loading
})

<li><Link to="/i18n">i18n</Link></li>

<Route exact path="/i18n" component={TestI18N}/>

进入页面自行查看效果


i18n (i18n-js)

npm i i18n-js lscache --save
// 创建相关文件
cd src && mkdir I18N
cd I18N && touch index.js en_US.js zh_CN.js

lscache: localstorage封装的库, 用于本地存储, 使用文档点这里 i18n-js: GitHub

首先在 common/instants.js 中添加

export const LOCAL_LANGUAGE = 'local_language'

I18N 目录中的文件补全代码

// index.js
import lscache from 'lscache'
import {LOCAL_LANGUAGE} from '../common/constants'
import I18N from 'i18n-js'
import en_US from './en_US'
import zh_CN from './zh_CN'

I18N.fallbacks = true
I18N.translations = {
    en_US,
    zh_CN
}
I18N.defaultLocale = 'zh_CN'
/**
 * 存放在 localstorage 中, 默认 'zh_CN'
 * 设置是只需要 lscache.set(LOCAL_LANGUAGE, 'en_US'), 并刷新页面
 */
const local = lscache.get(LOCAL_LANGUAGE)
I18N.locale = local ? local : 'zh_CN'

export default I18N

// en_US.js
export default {
    hello: 'This is a paragraph in English.',
    withParams: 'This is an international copy with parameters, {{param}}'
}

// zh_CN.js
export default {
    hello: '这是一段放在国际化文件中的中文.',
    withParams: '这是一段带有参数的国际化文案, {{param}}'
}

注入 I18N, 在 bootstrap/index.js 中添加

import i18n from '../I18N'

window.i18n = global.i18n = i18n

改造 Hello/index.js 用于测试

import React, {Component} from 'react';
import { Alert, Button } from "antd";
import lscache from 'lscache'
import {LOCAL_LANGUAGE} from '../../common/constants'

export default class Hello extends Component {
    render() {
        return <div>
            我是独立的 Hello 组件
            <div>{i18n.t('hello')}</div>
            <Alert message={i18n.t('withParams', {param: '--这个是参数--'})} type="success" />
            <Button onClick={() => lscache.set(LOCAL_LANGUAGE, 'en_US')}>en_US</Button>
            <div>
                <div>6kb</div>
                <img src={require('../../assets/images/antd6kb.png')} />
                <div>10kb</div>
                <img src={require('../../assets/images/antd10kb.png')} />
            </div>
        </div>
    }
}

相关代码 branch: i18n-v1


what performance: { hints: false }

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published