知乎日报APP
# 知乎日报WebApp
# 项目初始化
create-react-app zhihu_web
暴露配置项
yarn eject
+ 配置less:less/less-loader@8
+ 配置别名 @ 代表 src 目录「选配」
+ 配置浏览器兼容
+ 配置客户端启动服务的信息
+ 配置跨域代理:http-proxy-middleware
+ 配置REM响应式布局的处理:lib-flexible、postcss-pxtorem
+ 配置打包优化
2
3
4
5
6
7
配置别名 webpack.config.js
//313
resolve: {
...
alias: {
'@': paths.appSrc,
...
}
}
2
3
4
5
6
7
8
9
配置less:less/less-loader@8
yarn add less less-loader@8
config/webpack.config.js默认是支持scss,需要将scss的配置拷贝一份,换成less
src下新建setupProxy.js
配置跨域
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
createProxyMiddleware("/api", {
target: "http://127.0.0.1:7100",
changeOrigin: true,
ws: true,
pathRewrite: { "^/api": "" }
})
);
};
2
3
4
5
6
7
8
9
10
11
配置样式
src/assets/rest.min.css
body,h1,h2,h3,h4,h5,h6,hr,p,blockquote,dl,dt,dd,ul,ol,li,button,input,textarea,th,td{margin:0;padding:0}body{font-size:12px;font-style:normal;font-family:"\5FAE\8F6F\96C5\9ED1",Helvetica,sans-serif}small{font-size:12px}h1{font-size:18px}h2{font-size:16px}h3{font-size:14px}h4,h5,h6{font-size:100%}ul,ol{list-style:none}a{text-decoration:none;background-color:transparent}a:hover,a:active{outline-width:0;text-decoration:none}table{border-collapse:collapse;border-spacing:0}hr{border:0;height:1px}img{border-style:none}img:not([src]){display:none}svg:not(:root){overflow:hidden}html{-webkit-touch-callout:none;-webkit-text-size-adjust:100%}input,textarea,button,a{-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]),video:not([controls]){display:none;height:0}progress{vertical-align:baseline}mark{background-color:#ff0;color:#000}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}button,input,select,textarea{font-size:100%;outline:0}button,input{overflow:visible}button,select{text-transform:none}textarea{overflow:auto}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.clearfix:after{display:block;height:0;content:"";clear:both}
utils.js
/* 检测数据类型 */
const class2type = {},
toString = class2type.toString,
hasOwn = class2type.hasOwnProperty;
const toType = function toType(obj) {
let reg = /^\[object ([\w\W]+)\]$/;
if (obj == null) return obj + "";
return typeof obj === "object" || typeof obj === "function" ?
reg.exec(toString.call(obj))[1].toLowerCase() :
typeof obj;
};
const isFunction = function isFunction(obj) {
return typeof obj === "function" &&
typeof obj.nodeType !== "number" &&
typeof obj.item !== "function";
};
const isWindow = function isWindow(obj) {
return obj != null && obj === obj.window;
};
const isArrayLike = function isArrayLike(obj) {
let length = !!obj && "length" in obj && obj.length,
type = toType(obj);
if (isFunction(obj) || isWindow(obj)) return false;
return type === "array" || length === 0 ||
(typeof length === "number" && length > 0 && (length - 1) in obj);
};
const isPlainObject = function isPlainObject(obj) {
let proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true;
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
const isEmptyObject = function isEmptyObject(obj) {
let keys = Object.getOwnPropertyNames(obj);
if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
return keys.length === 0;
};
const isNumeric = function isNumeric(obj) {
var type = toType(obj);
return (type === "number" || type === "string") &&
!isNaN(obj - parseFloat(obj));
};
/* 函数的防抖和节流 */
const clearTimer = function clearTimer(timer) {
if (timer) clearTimeout(timer);
return null;
};
const debounce = function debounce(func, wait, immediate) {
if (typeof func !== "function") throw new TypeError("func is not a function!");
if (typeof wait === "boolean") {
immediate = wait;
wait = undefined;
}
wait = +wait;
if (isNaN(wait)) wait = 300;
if (typeof immediate !== "boolean") immediate = false;
let timer = null;
return function operate(...params) {
let now = !timer && immediate;
timer = clearTimer(timer);
timer = setTimeout(() => {
if (!immediate) func.call(this, ...params);
timer = clearTimer(timer);
}, wait);
if (now) func.call(this, ...params);
};
};
const throttle = function throttle(func, wait) {
if (typeof func !== "function") throw new TypeError("func is not a function!");
wait = +wait;
if (isNaN(wait)) wait = 300;
let timer = null,
previous = 0;
return function operate(...params) {
let now = +new Date(),
remaining = wait - (now - previous);
if (remaining <= 0) {
func.call(this, ...params);
previous = +new Date();
timer = clearTimer(timer);
} else if (!timer) {
timer = setTimeout(() => {
func.call(this, ...params);
previous = +new Date();
timer = clearTimer(timer);
}, remaining);
}
};
};
/* 数组和对象的操作 */
const mergeArray = function mergeArray(first, second) {
if (typeof first === "string") first = Object(first);
if (typeof second === "string") second = Object(second);
if (!isArrayLike(first)) first = [];
if (!isArrayLike(second)) second = [];
let len = +second.length,
j = 0,
i = first.length;
for (; j < len; j++) {
first[i++] = second[j];
}
first.length = i;
return first;
};
const each = function each(obj, callback) {
let isArray = isArrayLike(obj),
isObject = isPlainObject(obj);
if (!isArray && !isObject) throw new TypeError('obj must be a array or likeArray or plainObject');
if (!isFunction(callback)) throw new TypeError('callback is not a function');
if (isArray) {
for (let i = 0; i < obj.length; i++) {
let item = obj[i],
index = i;
if (callback.call(item, item, index) === false) break;
}
return obj;
}
let keys = Object.getOwnPropertyNames(obj);
if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
for (let i = 0; i < keys.length; i++) {
let key = keys[i],
value = obj[key];
if (callback.call(value, value, key) === false) break;
}
return obj;
};
const merge = function merge(...params) {
let options,
target = params[0],
i = 1,
length = params.length,
deep = false,
treated = params[length - 1];
toType(treated) === 'set' ? length-- : treated = new Set();
if (typeof target === "boolean") {
deep = target;
target = params[i];
i++;
}
if (target == null || (typeof target !== "object" && !isFunction(target))) target = {};
for (; i < length; i++) {
options = params[i];
if (options == null) continue;
if (treated.has(options)) return options;
treated.add(options);
each(options, (copy, name) => {
let copyIsArray = Array.isArray(copy),
copyIsObject = isPlainObject(copy),
src = target[name];
if (deep && copy && (copyIsArray || copyIsObject)) {
if (copyIsArray && !Array.isArray(src)) src = [];
if (copyIsObject && !isPlainObject(src)) src = {};
target[name] = merge(deep, src, copy, treated);
} else if (copy !== undefined) {
target[name] = copy;
}
});
}
return target;
};
const clone = function clone(...params) {
let target = params[0],
deep = false,
length = params.length,
i = 1,
isArray,
isObject,
result,
treated;
if (typeof target === "boolean" && length > 1) {
deep = target;
target = params[1];
i = 2;
}
treated = params[i];
if (!treated) treated = new Set();
if (treated.has(target)) return target;
treated.add(target);
isArray = Array.isArray(target);
isObject = isPlainObject(target);
if (target == null) return target;
if (!isArray && !isObject && !isFunction(target) && typeof target === "object") {
try {
return new target.constructor(target);
} catch (_) {
return target;
}
}
if (!isArray && !isObject) return target;
result = new target.constructor();
each(target, (copy, name) => {
if (deep) {
result[name] = clone(deep, copy, treated);
return;
}
result[name] = copy;
});
return result;
};
/* 设定具备有效期的localStorage存储方案 */
const storage = {
set(key, value) {
localStorage.setItem(key, JSON.stringify({
time: +new Date(),
value
}));
},
get(key, cycle = 2592000000) {
cycle = +cycle;
if (isNaN(cycle)) cycle = 2592000000;
let data = localStorage.getItem(key);
if (!data) return null;
let { time, value } = JSON.parse(data);
if ((+new Date() - time) > cycle) {
storage.remove(key);
return null;
}
return value;
},
remove(key) {
localStorage.removeItem(key);
}
};
/* 日期格式化 */
const formatTime = function formatTime(time, template) {
if (typeof time !== "string") {
time = new Date().toLocaleString('zh-CN', { hour12: false });
}
if (typeof template !== "string") {
template = "{0}年{1}月{2}日 {3}:{4}:{5}";
}
let arr = [];
if (/^\d{8}$/.test(time)) {
let [, $1, $2, $3] = /^(\d{4})(\d{2})(\d{2})$/.exec(time);
arr.push($1, $2, $3);
} else {
arr = time.match(/\d+/g);
}
return template.replace(/\{(\d+)\}/g, (_, $1) => {
let item = arr[$1] || "00";
if (item.length < 2) item = "0" + item;
return item;
});
};
const utils = {
toType,
isFunction,
isWindow,
isArrayLike,
isPlainObject,
isEmptyObject,
isNumeric,
debounce,
throttle,
mergeArray,
each,
merge,
clone,
storage,
formatTime
};
/* 处理冲突 */
if (typeof window !== "undefined") {
let $ = window._;
utils.noConflict = function noConflict() {
if (window._ === utils) {
window._ = $;
}
return utils;
};
}
/* 导出API */
export default utils;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
请求封装
src/api/http.js
import _ from '../assets/utils';
import qs from 'qs';
import { Toast } from 'antd-mobile';
/* 核心方法 */
const http = function http(config) {
// initial config & validate
if (!_.isPlainObject(config)) config = {};
config = Object.assign({
url: '',
method: 'GET',
credentials: 'include',
headers: null,
body: null,
params: null,
responseType: 'json',
signal: null
}, config);
if (!config.url) throw new TypeError('url must be required');
if (!_.isPlainObject(config.headers)) config.headers = {};
if (config.params !== null && !_.isPlainObject(config.params)) config.params = null;
let { url, method, credentials, headers, body, params, responseType, signal } = config;
if (params) {
url += `${url.includes('?') ? '&' : '?'}${qs.stringify(params)}`;
}
if (_.isPlainObject(body)) {
body = qs.stringify(body);
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
// 处理Token
let token = _.storage.get('tk'),
safeList = ['/user_info', '/user_update', '/store', '/store_remove', '/store_list'];
if (token) {
let reg = /\/api(\/[^?#]+)/,
[, $1] = reg.exec(url) || [];
let isSafe = safeList.some(item => {
return $1 === item;
});
if (isSafe) headers['authorization'] = token;
}
// send
method = method.toUpperCase();
config = {
method,
credentials,
headers,
cache: 'no-cache',
signal
};
if (/^(POST|PUT|PATCH)$/i.test(method) && body) config.body = body;
return fetch(url, config)
.then(response => {
let { status, statusText } = response;
if (/^(2|3)\d{2}$/.test(status)) {
let result;
switch (responseType.toLowerCase()) {
case 'text':
result = response.text();
break;
case 'arraybuffer':
result = response.arrayBuffer();
break;
case 'blob':
result = response.blob();
break;
default:
result = response.json();
}
return result;
}
return Promise.reject({
code: -100,
status,
statusText
});
})
.catch(reason => {
Toast.show({
icon: 'fail',
content: '网络繁忙,请稍后再试!'
});
return Promise.reject(reason);
});
};
/* 快捷方法 */
["GET", "HEAD", "DELETE", "OPTIONS"].forEach(item => {
http[item.toLowerCase()] = function (url, config) {
if (!_.isPlainObject(config)) config = {};
config['url'] = url;
config['method'] = item;
return http(config);
};
});
["POST", "PUT", "PATCH"].forEach(item => {
http[item.toLowerCase()] = function (url, body, config) {
if (!_.isPlainObject(config)) config = {};
config['url'] = url;
config['method'] = item;
config['body'] = body;
return http(config);
};
});
export default http;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
响应式
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<title>REM练习</title>
<!-- IMPORT CSS -->
<link rel="stylesheet" href="src/assets/reset.min.css">
<style>
/*
实现REM响应式布局开发的步骤
@1 找参照的比例(例如设计稿的比例 -> 一般都是750px),在这个比例下,给予html.fontSize一个初始值
html {
font-size: 100px;
// 750PX的设计稿中,1REM=100PX
// 未来我们需要把从设计稿中测量出来的尺寸(PX单位)转换为REM单位去设置样式
}
@2 我们需要根据当前设备的宽度,计算相对于设计稿750来讲,缩放的比例;从而让REM的转换比例,也跟着缩放「REM和PX的换算比例一改,则所有之前以REM为单位的样式也会跟着缩放」;
@3 我们一般还会给页面设置最大宽度「750px」,超过这个宽度,不再让REM比例继续变大了;内容居中,左右两边空出来即可!!
*/
html {
font-size: 100px;
}
html,
body {
height: 100%;
background: #F4F4F4;
}
#root {
margin: 0 auto;
max-width: 750px;
height: 100%;
background: #FFF;
}
.box {
width: 3.28rem;
height: 1.64rem;
line-height: 1.64rem;
text-align: center;
font-size: .4rem;
background: lightblue;
}
</style>
<script>
/* 计算当前设备下,REM和PX的换算比例 */
(function () {
const computed = () => {
let html = document.documentElement,
deviceW = html.clientWidth,
designW = 750;
if (deviceW >= designW) {
html.style.fontSize = '100px';
return;
}
let ratio = deviceW * 100 / designW;
html.style.fontSize = ratio + 'px';
};
computed();
window.addEventListener('resize', computed);
})();
</script>
</head>
<body>
<div id="root">
<div class="box">
rem响应式
</div>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
移动端响应式布局开发
<meta name="viewport" content="width=device-width, initial-scale=1.0">
一定要设置的
如果不设置,浏览器会按照980的宽度渲染页面;手机宽度不足980,整个页面就会整体缩小。
width=device-width HTML的渲染宽度和设备宽度保持一致initial-scale=1.0初始缩放比例:既不放大也不缩小
响应式处理
lib-flexible 设置REM和PX换算比例的
- 根据设备宽度的变化自动计算
- html.style.fontSize=设备的宽度/10+'px';
- 750设计稿中 1REM=75PX : 初始换算比例
- 375设备上 1REM=37.5PX
postcss-pxtorem 可以把我们写的PX单位,按照当时的换算比例,自动转换为REM,不需要我们自己算了
假设设计稿还是750的,我们测出来多少尺寸,我们写样式的时候,就写多少尺寸,并且不需要手动转换为REM「我们在webpack中,针对postcss-pxtorem做配置,让插件帮我们自动转换」
const px2rem = require('postcss-pxtorem'); px2rem({ rootValue: 75, // 基于lib-flexible,750设计稿,就会设置为1REM=75PX;此时在webpack编译的时候,我们也需要让px2rem插件,按照1REM=75PX,把我们测出来的并且编写的PX样式,自动转换为REM; propList: ['*'] // 对所有文件中的样式都生效{AntdMobile组件库中的样式} })
1
2
3
4
5在入口中,我们导入lib-flexible,确保在不同的设备上,可以等比例的对REM的换算比例进行缩放!!
手动设置:设备宽度超过750PX后,不再继续放大!!
使用
yarn add lib-flexible postcss-pxtorem
webpack.config.js
搜索postcss-loader后添加,条件1和条件2都添加
plugins: !useTailwind? [条件1] : [条件2]
src/index.js中引入
import 'lib-flexible';
src/index.less
直接写px,插件会帮我们转
@import './assets/reset.min.css';
#root {
width: 600px;
height: 600px;
background-color: red;
}
2
3
4
5
6
7
组件库
https://ant-design-mobile.antgroup.com/zh/guide/quick-start
yarn add antd-mobile
根据官方文档提示,配置兼容性
我们建议在项目中增加下面的 babel 配置,这样可以达到最大兼容性,为 iOS Safari >= 10
和 Chrome >= 49
:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "49",
"ios": "10"
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
13
# 路由配置
安装
yarn add react-router-dom
src/routes.js
路由表
import { lazy } from 'react';
import Home from '@/views/home';
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: {
title: '知乎日报-WebApp'
}
},
{
path: '/detail/:id',
name: 'detail',
component: lazy(() => import('@/views/detail')),
meta: {
title: '新闻详情-知乎日报'
}
},
{
path: '/personal',
name: 'personal',
component: lazy(() => import('@/views/person/index')),
meta: {
title: '个人中心-知乎日报'
}
},
{
path: '/collection',
name: 'collection',
component: lazy(() => import('@/views/collection')),
meta: {
title: '我的收藏-知乎日报'
}
},
{
path: '/update',
name: 'update',
component: lazy(() => import('@/views/update')),
meta: {
title: '修改个人信息-知乎日报'
}
},
{
path: '/login',
name: 'login',
component: lazy(() => import('@/views/login')),
meta: {
title: '登录/注册-知乎日报'
}
},
{
path: '*',
name: '404',
component: lazy(() => import('@/views/404')),
meta: {
title: '404页面-知乎日报'
}
}
]
export default routes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
src/index.js
import { Suspense } from "react";
import { Mask, DotLoading } from "antd-mobile";
import { Routes, Route, useNavigate, useLocation, useParams, useSearchParams } from "react-router-dom";
import routes from "./routes";
const Element = (props) => {
let { component: Component, meta } = props;
// 其他权限信息的验证
// 修改页面的TITLE
let { title = "知乎日报-WebApp" } = meta || {};
document.title = title;
const navigate = useNavigate(),
location = useLocation(),
params = useParams(),
[usp] = useSearchParams();
// 获取路由信息,基于属性传递给组件
return <Component
navigate={navigate}
location={location}
params={params}
usp={usp}
/>;
}
const RouterView = () => {
return <Suspense fallback={
<Mask visible={true}>
<DotLoading color="white" />
</Mask>
}>
<Routes>
{
routes.map((item) => {
let {path, name} = item
return <Route
key={name}
path={path}
element={<Element {...item} />}
/>;
})
}
</Routes>
</Suspense>
};
export default RouterView;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
src/App.jsx
import { HashRouter } from "react-router-dom";
import RouterView from "./router";
function App() {
return (
<HashRouter>
<RouterView/>
</HashRouter>
);
}
export default App;
2
3
4
5
6
7
8
9
10
11
# redux配置
安装
yarn add redux react-redux redux-logger redux-thunk redux-promise
src/store/action-types.js
export const BASE_INFO = "BASE_INFO";
export const COLLECTION_LIST = "COLLECTION_LIST";
export const COLLECTION_REMOVE = "COLLECTION_REMOVE";
2
3
4
src/store/index.js
import { createStore, applyMiddleware } from 'redux';
import reduxLogger from 'redux-logger';
import { thunk as reduxThunk } from 'redux-thunk';
import reduxPromise from 'redux-promise';
import reducer from './reducers';
// 根据不同的环境,使用不同的中间件
let middleware = [reduxThunk, reduxPromise],
env = process.env.NODE_ENV;
if (env !== 'production') {
middleware.push(reduxLogger);
}
// 创建store容器
const store = createStore(
reducer,
applyMiddleware(...middleware)
);
export default store;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/store/actions/baseAction.js
import * as TYPES from '../action-types';
const baseAction = {
};
export default baseAction;
2
3
4
5
6
src/store/actions/collectionAction.js
import * as TYPES from '../action-types';
const collectionAction = {
};
export default collectionAction;
2
3
4
5
6
src/store/actions/index.js
import baseAction from "./baseAction";
import collectionAction from "./collectionAction";
const action = {
baseAction,
collectionAction
}
export default action;
2
3
4
5
6
7
8
src/store/reducers/baseReducer.js
import * as TYPES from '../action-types';
import _ from '@/assets/utils';
let initial = {
info: null
};
export default function baseReducer(state = initial, action) {
state = _.clone(state);
switch (action.type) {
// ...
default:
}
return state;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
src/store/reducers/collectionReducer.js
import * as TYPES from '../action-types';
import _ from '@/assets/utils';
let initial = {
list: null
};
export default function collectionReducer(state = initial, action) {
state = _.clone(state);
switch (action.type) {
// ...
default:
}
return state;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
src/store/reducers/index.js
import { combineReducers } from 'redux';
import baseReducer from './baseReducer';
import collectionReducer from './collectionReducer';
const reducer = combineReducers({
base: baseReducer,
collection: collectionReducer
});
export default reducer;
2
3
4
5
6
7
8
9
index.jsx中引入
import React from 'react';
import ReactDOM from 'react-dom/client';
/* REDUX */
import { Provider } from 'react-redux';
import store from './store';
/* ANTD-MOBILE */
import { ConfigProvider } from 'antd-mobile';
import zhCN from 'antd-mobile/es/locales/zh-CN';
import App from './App';
import 'lib-flexible';
import './index.less';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ConfigProvider locale={zhCN}>
<Provider store={store}>
<App />
</Provider>
</ConfigProvider>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
接口
src/api.js
import http from './http';
// 获取今日新闻信息 & 轮播图信息
const queryNewsLatest = () => http.get('/api/news_latest');
// 获取往日新闻信息
const queryNewsBefore = (time) => {
return http.get('/api/news_before', {
params: {
time
}
});
};
// 获取新闻详细信息
const queryNewsInfo = (id) => {
return http.get('/api/news_info', {
params: {
id
}
});
};
// 获取新闻点赞信息
const queryStoryExtra = (id) => {
return http.get('/api/story_extra', {
params: {
id
}
});
};
// 发送验证码
const sendPhoneCode = (phone) => {
return http.post('/api/phone_code', {
phone
});
};
// 登录/注册
const login = (phone, code) => {
return http.post('/api/login', {
phone,
code
});
};
// 获取登录者信息
const queryUserInfo = () => http.get('/api/user_info');
// 收藏新闻
const collection = (newsId) => {
return http.post('/api/store', { newsId });
};
// 移除收藏
const collectionRemove = (id) => {
return http.get('/api/store_remove', {
params: {
id
}
});
};
// 获取收藏列表
const collectionList = () => http.get('/api/store_list');
// 图片上传「要求FormData格式」
const upload = (file) => {
let fm = new FormData();
fm.append('file', file);
return http.post('/api/upload', fm);
};
// 修改个人信息
const userUpdate = (username, pic) => {
return http.post('/api/user_update', {
username,
pic
});
};
/* 暴露API */
const api = {
queryNewsLatest,
queryNewsBefore,
queryNewsInfo,
queryStoryExtra,
sendPhoneCode,
login,
queryUserInfo,
collection,
collectionRemove,
collectionList,
upload,
userUpdate
};
export default api;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 通用组件封装
在真实项目中,我们的组件:
普通业务组件「SPA中的一个个页面」一般写在src/views
通用业务组件:好多页面中都需要的,我们提取成为公共的组件src/components
特色亮点,擅于发现通用的部分,进行封装和提取
保证更强的复用性:属性、插槽.....
通用功能组件:一般都是UI组件库中有的
偶尔有部分UI组件库中没有或者不支持的,才需要自己进行封装
例如:大文件切片上传和断点续传!「UI组件库中,一般文件处理类、影音类的部比较匮乏」但是我们一般会对UI组件库中的组件,进行二次封装:
- 统一处理复杂的业务逻辑
- 统一处理样式
- 几个组件做为一个整体组件