vue3项目实战---知乎日报----项目搭建

目录

基础框架和响应式布局

项目介绍

接口文档

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地址 知乎icon-default.png?t=M3K6https://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 布局适配

  • postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位
  • amfe-flexible设置根元素字体大小

  • main.js 导入import 'amfe-flexible'(按照设备的宽度/10)

  • 在根目录下创建// postcss.config.js 对其进行配置

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: ['*'],
    },
  },
};

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值