https://github.com/ariesly15/webpack-antd-demo.git
项目托管于GitHub, 文档很多, 需要有耐心
1.本教程基于mac环境
`` sh
node -v
# v6.9.5
npm -v
# 3.10.10
%%CODEBLOCK0%% sh
cd src/pages
mkdir Home
%%CODEBLOCK1%%
webpack-antd-demo
├── antd@3.8.4
├── react@16.4.2
├── react-dom@16.4.2
└── webpack@4.17.1
%%CODEBLOCK2%%
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 线上配置
%%CODEBLOCK3%% sh
mkdir webpack-antd-demo && cd webpack-antd-demo
npm init
# 按照提示填写基本信息或者用 npm init -y 一步创建完成
%%CODEBLOCK4%% sh
npm i webpack webpack-cli --save-dev
# --save 线上依赖
# --save-dev 开发依赖
%%CODEBLOCK5%% sh
touch webpack.dev.js
%%CODEBLOCK6%% 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'
}
}
%%CODEBLOCK7%% sh
mkdir src && touch ./src/index.js
%%CODEBLOCK8%% js
// ./src/index.js 内容
document.getElementById('app').innerHTML = "webpack-antd-demo"
%%CODEBLOCK9%% json
// 在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"
}
}
%%CODEBLOCK10%% sh
npm run dev
%%CODEBLOCK11%% sh
# 在dist目录创建html文件
touch ./dist/index.html
%%CODEBLOCK12%% 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>
%%CODEBLOCK13%% sh
# 安装
npm i --save-dev babel-loader@8 @babel/core @babel/preset-env
%%CODEBLOCK14%% sh
touch .babelrc
%%CODEBLOCK15%% json
// .babelrc 内容
{
"presets": ["@babel/preset-env"],
"plugins": []
}
%%CODEBLOCK16%% js
// 修改webpack.dev.js,增加babel-loader!
module: {
rules: [{
test: /\.js$/, // 正则匹配以 .js 结尾的文件来使用 babel 解析
use: ['babel-loader?cacheDirectory=true'], // cacheDirectory是用来缓存编译结果,下次编译加速
include: path.join(__dirname, 'src') // 需要解析的目录
}]
}
%%CODEBLOCK17%% js
// 修改 ./src/index.js 使用es6的箭头函数
const useBabel = text => document.getElementById('app').innerHTML = text
useBabel('正在使用babel')
%%CODEBLOCK18%% sh
npm i --save react react-dom
%%CODEBLOCK19%% js
import React from 'react'
import {render} from 'react-dom'
render(
<div>Hello React !!!</div>,
document.getElementById('app')
)
%%CODEBLOCK20%% sh
npm i @babel/preset-react --save-dev
%%CODEBLOCK21%% js
// 修改后的内容
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": []
}
%%CODEBLOCK22%% sh
cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch index.js
%%CODEBLOCK23%% jsx
import React, {Component} from 'react';
export default class Hello extends Component {
render() {
return <div>
我是独立的 Hello 组件
</div>
}
}
%%CODEBLOCK24%% jsx
// 修改 ./src/index.js 为如下内容
import React from 'react'
import {render} from 'react-dom'
import Hello from './component/Hello'
render(
<Hello/>,
document.getElementById('app')
)
%%CODEBLOCK25%% sh
# 安装antd
npm i antd --save
%%CODEBLOCK26%% sh
npm i babel-plugin-import --save-dev
%%CODEBLOCK27%% js
// 修改 .babelrc 文件, 修改后的文件内容如下
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["import",{
"libraryName": "antd",
"style": "css"
}]
]
}
%%CODEBLOCK28%% js
// babel-plugin-import 会帮助你加载 JS 和 CSS
import { Alert } from 'antd';
%%CODEBLOCK29%% sh
npm i css-loader style-loader --save-dev
%%CODEBLOCK30%% js
// 修改 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']
}
]
}
};
%%CODEBLOCK31%% js
// 修改后的 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>
}
}
%%CODEBLOCK32%% sh
npm i html-webpack-plugin --save-dev
%%CODEBLOCK33%% sh
cd src && touch index.html
%%CODEBLOCK34%% 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>
%%CODEBLOCK35%% 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')
})
]
};
%%CODEBLOCK36%% sh
# 安装
npm i --save react-router-dom
# 新建router文件夹和组件
cd src
mkdir router && touch router/index.js
%%CODEBLOCK37%% 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>
}
}
%%CODEBLOCK38%% js
// 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
%%CODEBLOCK39%% js
// 把
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom'
// 修改为
import {HashRouter as Router, Route, Switch, Link} from 'react-router-dom'
%%CODEBLOCK40%% sh
# 安装
npm i webpack-dev-server --save-dev
%%CODEBLOCK41%% js
module.exports = {
//...
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 9090
}
}
%%CODEBLOCK42%% js
{
// ...
"scripts": {
// ...
"server": "webpack-dev-server --config webpack.dev.js --mode development"
},
// ...
}
%%CODEBLOCK43%% sh
npm i --save-dev url-loader file-loader
%%CODEBLOCK44%% js
// 修改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
}
}]
}
]
},
// ...
}
%%CODEBLOCK45%% sh
# 创建图片目录
mkdir -p src/assets/images
%%CODEBLOCK46%% js
// 修改 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>
%%CODEBLOCK47%% sh
# 安装 react-loadable
npm i react-loadable --save
# 安装 babel-plugin-syntax-dynamic-import 来支持react-loadable的import方法
npm i babel-plugin-syntax-dynamic-import --save-dev
%%CODEBLOCK48%% js
{
// ...
"plugins": [
// ...
"syntax-dynamic-import"
]
}
%%CODEBLOCK49%% js
// 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
}
}
}
%%CODEBLOCK50%% 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
})
%%CODEBLOCK51%% sh
npm i @babel/plugin-proposal-class-properties --save-dev
%%CODEBLOCK52%% js
{
// ...
"plugins": [
// ...
"@babel/plugin-proposal-class-properties"
]
}
%%CODEBLOCK53%% 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>
}
}
%%CODEBLOCK54%% js
module.exports = {
// ...
optimization: {
runtimeChunk: 'single'
}
}
%%CODEBLOCK55%% 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"
}
}
}
}
}
%%CODEBLOCK56%% sh
touch webpack.prd.js
%%CODEBLOCK57%% 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"
}
}
}
}
};
%%CODEBLOCK58%% js
// 修改package.json, 添加scripts
{
// ...
"scripts": {
// ...
"prod": "webpack --config webpack.prod.js --color --progress"
},
// ...
}
%%CODEBLOCK59%% sh
# 创建公共配置
touch webpack.common.js
# 安装 webpack-merge
npm i webpack-merge --save-dev
%%CODEBLOCK60%% js
// 修改 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"
},
// ...
}
%%CODEBLOCK61%% js
// 抽取公共配置到 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);
%%CODEBLOCK62%% sh
npm i clean-webpack-plugin --save-dev
%%CODEBLOCK63%% js
// webpack.common.js
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin(['dist'])
]
%%CODEBLOCK64%% sh
# 安装
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
%%CODEBLOCK65%% 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}]
]
}
%%CODEBLOCK66%% jsx
// 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'))
%%CODEBLOCK67%% jsx
// 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>
}
}
%%CODEBLOCK68%% sh
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
%%CODEBLOCK69%% js
// src/index.js 添加两行代码
import './bootstrap'
import "@babel/polyfill";
%%CODEBLOCK70%% js
// 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})
}
}
%%CODEBLOCK71%% js
// bootstrap/index.js render之前进行注入的一些代码, 例如请求拦截器
import './http-interceptors'
%%CODEBLOCK72%% js
// 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)
}
}
%%CODEBLOCK73%% js
/**
* 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
}
}
%%CODEBLOCK74%% jsx
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>
}
}
%%CODEBLOCK75%% 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
}
}
}
});
%%CODEBLOCK76%% js
// store/index.js
import UiStore from './ui'
export const uiStore = new UiStore()
%%CODEBLOCK77%% jsx
// 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}/>
%%CODEBLOCK78%% sh
mkdir src/component/NotFound
touch src/component/NotFound/index.js
%%CODEBLOCK79%% jsx
// NotFount/index.js
import React, {PureComponent} from 'react'
export default class NotFound extends PureComponent {
render(){
return <div style={{color: 'red', fontSize: 88}}>404</div>
}
}
%%CODEBLOCK80%% jsx
// router/index.js
<Switch>
// ...
<Route component={NotFound}/>
</Switch>
%%CODEBLOCK81%% sh
npm i mini-css-extract-plugin less less-loader --save-dev
%%CODEBLOCK82%% json
{
// ...
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js --color --progress",
"build": "webpack --config webpack.prod.js --color --progress"
}
// ...
}
%%CODEBLOCK83%% 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"
}
}
}
}
};
%%CODEBLOCK84%% 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
}
}
}
});
%%CODEBLOCK85%% 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"
})
]
})
%%CODEBLOCK86%% sh
# 因为sass-loader依赖于node-sass,所以需要先安装node-sass
npm i sass node-sass --save-dev
npm i sass-loader --save-dev
%%CODEBLOCK87%% js
{
test: /\.s(a|c)ss$/,
use: ["style-loader","css-loader","sass-loader"]
}
%%CODEBLOCK88%% js
{
test: /\.s(a|c)ss$/,
use: [MiniCssExtractPlugin.loader,"css-loader","sass-loader"]
}
%%CODEBLOCK89%% sh
cd src
mkdir pages
cd common && touch utils.js contants.js
%%CODEBLOCK90%% sh
npm i react-intl intl -save
// 创建相关文件
cd src && mkdir I18N && touch app.js
cd I18N && touch en_US.js zh_CN.js
%%CODEBLOCK91%% js
// 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>
);
}
}
%%CODEBLOCK92%% js
// 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}'
}
%%CODEBLOCK93%% js
@observable language = 'zh'
@action
updateLanguage(lang){
this.language = lang
}
%%CODEBLOCK94%% js
// 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>
);
}
}
%%CODEBLOCK95%% js
const TestI18N = Loadable({
loader: () => import('../component/Hello/TestI18N'),
loading: Loading
})
<li><Link to="/i18n">i18n</Link></li>
<Route exact path="/i18n" component={TestI18N}/>
%%CODEBLOCK96%% sh
npm i i18n-js lscache --save
// 创建相关文件
cd src && mkdir I18N
cd I18N && touch index.js en_US.js zh_CN.js
%%CODEBLOCK97%% js
export const LOCAL_LANGUAGE = 'local_language'
%%CODEBLOCK98%% js
// 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}}'
}
%%CODEBLOCK99%% js
import i18n from '../I18N'
window.i18n = global.i18n = i18n
%%CODEBLOCK100%% 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