目录
基础框架和响应式布局
项目介绍
接口文档
vue.config
pagejson
初始化公共样式
vuex模块
路由模块
utils公共类库
axios 二次封装
响应式处理 && vant ui组件库
基础框架和响应式布局
项目介绍
知乎日报
@1 技术栈:@vue/cli、vue3、vuex4、vue-router4、vant3、axios(qs)...
+ postcss-pxtorem & amfe-flexible 基于这两个模块实现移动端适配
+ less & less-loader
+ babel-plugin-import 实现UI组件库的按需导入
@2 需要完成的页面
+ 首页:新闻列表、头部、轮播图... /
+ 详情页:文章内容、底部操作栏 /detail/:id
+ 个人中心 /person
+ 收藏列表 /store
+ 登录页面 /login
+ 修改个人信息 /update
@3 开始搭建项目的架子
+ 基于@vue/cli创建项目 & 配置 vue.config.js & 处理浏览器兼容 & 安装需要的模块...
+ 在SRC目录中
+ 配置vuex
+ 配置vue-router
+ 配置api
+ 配置响应式布局方案 & 导入UI组件库
+ 然后一个组件一个组件的去开发即可「规划组件划分」
git地址 知乎https://gitee.com/childe-jia/backstage.git
接口文档
总述:
POST模式,基于请求主体传递给服务器的数据格式 application/x-www-form-urlencoded 「/user_update接口特殊」
GET模式,基于“问号参数”传递数据
初始账号:珠峰培训 13161883402
服务器返回信息中{返回的数据格式:JSON}
code :0成功 1失败
codeText :对状态码的描述
package.json
"config": {
"server": 7100, //服务器启动WEB服务的端口号
"secret": "ZFPX", //TOKEN加密秘钥
"maxAge": "7d" //TOKEN有效周期(7天)
}
database存储相关的数据
static存储用户上传的头像
本后台不支持CORS跨域请求,请客户端基于Proxy跨域代理实现
=======================
获取最新新闻 /news_latest GET
@params
@return
{
date:'20191204',
stories:[{
"image_hue": "0x484825",
"title": "如何评价 12 月 4 号国行 Switch 发布会?",
"url": "https:\/\/daily.zhihu.com\/story\/9717893",
"hint": "WouldYouKindly · 7 分钟阅读",
"ga_prefix": "120416",
"images": ["https:\/\/pic2.zhimg.com\/v2-8d47848eb5ea9d747b12eead9dbf4741.jpg"],
"type": 0,
"id": 9717893
},...],
top_stories:[{
"image_hue": "0x879943",
"hint": "作者 \/ 苏澄宇",
"url": "https:\/\/daily.zhihu.com\/story\/9717531",
"image": "https:\/\/pic2.zhimg.com\/v2-5c87a645d36d140fa87df6e8ca7cb989.jpg",
"title": "斑马的条纹到底是干嘛用的?",
"ga_prefix": "120407",
"type": 0,
"id": 9717531
},...]
}
获取以往新闻 /news_before GET
@params
time:20211022 传递今天日期则获取昨日新闻(不传则默认是今天日期)
@return
{
"date": "20191201",
"stories": [{
"image_hue": "0xb39f7d",
"title": "小事 · 芳姐",
"url": "https:\/\/daily.zhihu.com\/story\/9717813",
"hint": "VOL.1146",
"ga_prefix": "120122",
"images": ["https:\/\/pic1.zhimg.com\/v2-26cbb28e78f2fd51d786d9ef542f9358.jpg"],
"type": 0,
"id": 9717813
},...]
}
获取新闻详细信息 /news_info GET
@params
id:新闻ID
@return
{
"body": "",
"image_hue": "0x879943",
"image_source": "12019 \/ CC0",
"title": "斑马的条纹到底是干嘛用的?",
"url": "https:\/\/daily.zhihu.com\/story\/9717531",
"image": "https:\/\/pic2.zhimg.com\/v2-5c87a645d36d140fa87df6e8ca7cb989.jpg",
"share_url": "http:\/\/daily.zhihu.com\/story\/9717531",
"js": [],
"ga_prefix": "120407",
"images": ["https:\/\/pic4.zhimg.com\/v2-e5c464d7196a5fa254724cc91c15ca4b.jpg"],
"type": 0,
"id": 9717531,
"css": ["http:\/\/news-at.zhihu.com\/css\/news_qa.auto.css?v=4b3e3"]
}
获取新闻点赞信息 /story_extra GET
@params
id:新闻ID
@return
{
"long_comments": 1, 长评论总数
"popularity": 183, 点赞总数
"short_comments": 22, 短评论总数
"comments": 23 评论总数
}
用户登录 /login POST
@params
phone:手机号码
code:短信验证码 用户存在则为登录,不存在则为注册新用户
@return
{
code:0,
codeText:'',
token:''
}
获取手机验证码 /phone_code POST
@params
phone:手机号
@return
{
code:0,
codeText:''
}
临时测试:生成的验证码可在「后端程序 -> code.txt」中查看
=======================
以下接口需要在请求头中携带TOKEN信息
authorzation:token「客户端登录成功后存储在本地的令牌信息(从服务器获取)」
检测是否登录 /check_login GET
@params
@return
code
codeText
data:{
id,
name,
phone,
pic
}
获取登录者信息 /user_info GET
@params
@return
code
codeText
data:{
id,
name,
phone,
pic
}
修改用户名和头像 /user_update POST
@params 格式:multipart/form-data
username
file
@return
code
codeText
data:{
id,
name,
phone,
pic
}
收藏新闻 /store POST
@params
newsId:新闻ID
@return
code
codeText
移除收藏 /store_remove GET
@params
id:收藏ID
@return
code
codeText
获取登录者收藏列表 /store_list GET
@params
@return
code
codeText
data:[
{
id,
userId,
news: {
id,
title,
image
}
},...
]
vue.config
const ENV = process.env.NODE_ENV;
module.exports = {
lintOnSave: ENV !== 'production',
publicPath: './',
productionSourceMap: false,
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:7100',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};
pagejson
{
"name": "zhihu",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"amfe-flexible": "^2.2.1",
"axios": "^0.23.0",
"blueimp-md5": "^2.19.0",
"core-js": "^3.6.5",
"postcss-pxtorem": "^5.1.1",
"qs": "^6.10.1",
"vant": "^3.2.5",
"vue": "^3.2.26",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-import": "^1.13.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"less": "^3.13.1",
"less-loader": "^7.3.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
初始化公共样式
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}
vuex模块
vuex解构函数 createStore 创建store
main.js注册使用 app.use(store)
组件使用 vuex解构函数 useStore 创建store 只能通过 store.state.xxx store.commit/displach,派发任务,不能使用辅助函数
import { createStore } from "vuex";
const store = createStore({
state: {},
mutations: {},
actions: {},
})
export default store
--------------------------------mian.js
import { createApp } from 'vue'
import App from './App.vue'
import store from '../src/store/index'
const app = createApp(App)
app.use(store)
app.mount('#app')
--------------------------------组件
<script>
import { useStore } from "vuex";
export default {
name: 'App',
components: {},
setup() {
const store = useStore()
console.log(store);
},
}
路由模块
- route 路由信息对象:path params(路径参数) hash query name
- router是路由实例 路径的跳转方式(push require)
- 必须指定路由模式 history: createWebHashHistory(),
- //createWebHistory-->History路由 createWebHashHistory-->HASH路由
- main.js注册使用 app.use(router)
- 组件使用路由从 vue-router 解构 useRoute, useRouter
- 路由匹配后,放到 路由容器中<router-view></router-view>
---------------------路由信息
import Home from '@/views/Home.vue';
const routes = [{
path: '/',
name: 'Home',
meta: { title: '知乎日报' },
component: Home
}, {
path: '/detail/:id',
name: 'Detail',
meta: { title: '文章详情' },
component: () => import(/* webpackChunkName: "other" */'@/views/Detail.vue')
}, {
path: '/person',
name: 'Person',
meta: { title: '个人中心' },
component: () => import(/* webpackChunkName: "other" */'@/views/Person.vue')
}, {
path: '/store',
name: 'Store',
meta: { title: '收藏列表' },
component: () => import(/* webpackChunkName: "other" */'@/views/Store.vue')
}, {
path: '/update',
name: 'Update',
meta: { title: '编辑个人信息' },
component: () => import(/* webpackChunkName: "other" */'@/views/Update.vue')
}, {
path: '/login',
name: 'Login',
meta: { title: '用户登录' },
component: () => import(/* webpackChunkName: "other" */'@/views/Login.vue')
}, {
path: '/:pathMatch(.*)',
redirect: '/'
}];
export default routes;
--------------------------路由实例对象
import { createRouter, createWebHashHistory, } from "vue-router";
import routes from './routes';
const router = createRouter({
//createWebHistory-->History路由 createWebHashHistory-->HASH路由
//基于Hash指定模式
history: createWebHashHistory(),
routes,
})
export default router
---------------------mian.js
import { createApp } from 'vue'
import App from './App.vue'
import store from '../src/store/index'
import router from './router';
const app = createApp(App)
app.use(store)
app.use(router)
app.mount('#app')
---------------------app.vue
<template>
<router-view></router-view>
</template>
<script>
import { useRoute, useRouter } from "vue-router";
export default {
name: 'App',
components: {},
setup() {
const route = useRoute()
const router = useRouter()
console.log(router, route);
},
}
</script>
utils公共类库
// 检测是否为纯粹的对象
export const isPlainObject = function isPlainObject(obj) {
let proto, Ctor;
if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true;
Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
// 处理最大宽度
export const handleMaxWidth = function handleMaxWidth() {
let HTML = document.documentElement,
app = document.querySelector('#app'),
size = parseFloat(HTML.style.fontSize);
if (size > 75) {
HTML.style.fontSize = '75px';
app.style.width = '750px';
return;
}
app.style.width = '100%';
app.style.minHeight = HTML.clientHeight + 'px';
};
// 日期格式化
export 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;
});
};
axios 二次封装
- axios.defaults.timeout 超时时间
- axios.defaults.transformRequest post请求统一处理 为问号查询字符串格式。不写默认为json
- axios.interceptors.request.use请求拦截
- axios.interceptors.response.use 响应拦截
import axios from "axios";
import qs from 'qs';
import { isPlainObject } from '@/assets/utils';
import { Notify } from 'vant';
// 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。它可以通过设置一个 `baseURL`
axios.defaults.baseURL = '';
// 指定请求超时的毫秒数(0 表示无超时时间)
axios.defaults.timeout = 60000;
//允许在向服务器发送前,修改请求数据. 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
axios.defaults.transformRequest = data => {
if (isPlainObject(data)) return qs.stringify(data);
return data;
};
//在请求或响应被 then 或 catch 处理前拦截它们。添加请求拦截器
axios.interceptors.request.use(config => {
return config;
});
// 添加响应拦截器
axios.interceptors.response.use(response => {
return response.data;
}, reason => {
Notify({
type: 'danger',
message: '小主,当前网络繁忙,请稍后再试!'
});
return Promise.reject(reason);
});
export default axios;
响应式处理 && vant ui组件库
vant ui组件库
import { createApp } from 'vue'
import Vant from 'vant';
import 'vant/lib/index.css';
import App from './App.vue'
import store from '../src/store/index'
import router from './router';
const app = createApp(App)
app.use(store)
app.use(router)
app.use(Vant)
app.mount('#app')
底部安全区适配
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
Rem 布局适配
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*'],
},
},
};
原理
- vant 组件库本身就是以375的设计稿设计的(单位都为px)
- amfe-flexible 使用单位会根据375的设计稿 把html的font-size值设置为37.5
- 我们的目的是把px都转为rem postcss-pxtorem处理这件事
如果设计稿的尺寸不是 375,而是 750 或其他大小,可以将 rootValue
配置调整为:
判断是否为vant组件库 是为375 不是则为设计稿大小
// postcss.config.js
module.exports = {
plugins: {
// postcss-pxtorem 插件的版本需要 >= 5.0.0
'postcss-pxtorem': {
rootValue({ file }) {
return file.indexOf('vant') !== -1 ? 37.5 : 75;
},
propList: ['*'],
},
},
};