CATEGORIES

小说:现在什么挣钱快作者:邓徒更新时间:2018-12-12字数:92195

这时,她的贴身侍女上前道:“娘娘,明珠姑娘来了,在外面等候。”

赚网手机商城怎么样

纪太虚说着,顾欢三人耳边传来一声嘹亮的狼嚎,一道银光从天边冲来。
「阳不死」具有强烈毒性,而「阴不死」则稍微温和,但仅管温和,如果只吸阴而不吸阳的话,日子一久还是会全身长瘤,最后溃烂而亡,但一旦阴阳调合,则不仅对身体无害,还大有好处。

微微望了望窗外的夜景,唐欣觉得有些寂静。而后,发现自己不远处的房间的窗户有着人影闪过,随即那电灯的光芒消失,黑暗的一片。

CATEGORIES


前面的话

  笔者在做一个完整的博客上线项目,包括前台、后台、后端接口和服务器配置。本文将详细介绍使用vue全家桶制作的博客网站

 

概述

  该项目是基于vue全家桶(vue、vue-router、vuex、vue SSR)开发的一套博客前台页面,主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理

【访问地址】

  域名:https://xiaohuochai.cc

  Github: https://github.com/littlematch0123/blog-client

  或者可以直接扫描二维码访问

【项目介绍】

  该项目的内容以笔者自学前端的过程中写的600多篇博客为基础,对于同样学习前端的同学可能会有所帮助。许多博客都有直接可以操作的DEMO,对知识的理解可能会更直观

  采用移动优先的响应式布局,移动端、桌面端均可适配;字体大小使用em单位,桌面端的文字相应变大;移动端可使用滑屏操作,桌面端通过光标设置、自定义滚动条、回车确定等,提升交互体验

  全站采用服务器端渲染SSR的方式,有利于SEO,减少了首屏渲染时间;使用service worker和manifest实现了PWA方案的离线缓存和添加到桌面的功能

  根据HTML标签内容模型,使用语义化标签,尽量减少标签层级,尽量减少无语义的div标签

  CSS大量使用类选择器,尽量减少选择器层级,在vue组件中使用CSS module和postCSS,使用styleLint规范CSS代码,按照布局类属性、盒模型属性、文本类属性、修饰类属性的顺序编写代码,并使用order插件进行校验

  使用esLint规范JS代码,代码风格参照airbnb规范,所有命名采用驼峰写法,公共组件以Base为前缀,事件函数以on为前缀,异步函数以async为后缀,布尔值基本以do或is为前缀

  没有引用第三方组件库,如bootstrap或element组件,而是自己开发了项目中所需的公共组件。在common目录下,封装了头像、全屏、loading、遮罩、搜索框、联动选择等组件,方便开发

  使用配置数据,实现了数据和应用分离,以常量的形式存储在constants目录下

  使用了阿里云的短信模块,实现了短信验证功能

  该项目有两个隐藏彩蛋,一个是摇一摇功能,可以直接摇到后台页面,另一个是陀螺仪功能,上下晃动手机时,头像会进行旋转

  项目进行了代码优化,最终优化评分如下所示

 

功能演示

  主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理

【首页显示】

  首页包括可拖拽轮播图、专题推荐、文章推荐和类别推荐

【认证系统】

   认证系统包括用户注册、用户登录、短信验证

  1、用户处于未登录态时,可以阅读文章,但不能点赞和评论,否则会弹出登录框

  2、用户注册

  3、用户登录

【文章管理】

  文章管理包括浏览推荐文章、按类别筛选、文章搜索、按目录查看

  1、浏览推荐文章

  2、文章筛选

  3、文章搜索

  4、按目录查看

【点赞管理】

【评论管理】

  评论管理包括查看评论、添加评论、修改评论和删除评论

 

目录结构

  src目录下,包括assets(静态资源)、common(公共组件)、components(功能组件)、constants(常量配置)、router(路由)、store(vuex)和utils(工具方法)这7个目录

- assets // 存放静态资源,主要是图片
    -imgs
      css.png // CSS文章背景图
     ...
- common // 存放公共组件
    -SVG // 存放VUE图标组件
        SVGAdd.vue // "添加到"按钮
        SVGBack.vue // "返回"按钮
        ...
    BaseArticle.vue // 文章组件
    BaseAvatar.vue // 头像组件
    ...
- components // 存放功能组件
    -Post // 文章组件      
      module.js //文章状态管理    
      Post.vue // 文章显示组件
      PostContent.vue // 文章目录组件
      PostList.vue // 文章列表组件
      SearchPost.vue // 搜索文章组件
      ...
- constants // 存放常量配置
    API.js // 存放API调用地址
- router // 存放路由
    index.js 
- store // 存放vuex
    index.js
- utils // 存放工具方法
    async.js // axios方法
    fnVarificate.js // 表单验证方法
    util.js // 其他工具方法

【公共组件】

  没有引用第三方组件库,如bootstrap或element组件,而是自己开发了项目中所需的公共组件

  封装了文章组件、头像组件、返回组件、按钮组件、卡片组件、全屏组件、输入框组件、loading组件、遮罩组件、搜索框组件、多行输入框组件、标题组件、面包屑组件、按钮组组件、反色按钮组件、密码框组件、包含检测的输入框组件和联动选择组件

BaseAdd.vue // "添加到"组件
BaseArticle.vue  // 文章组件
BaseAvatar.vue // 头像组件
BaseBack.vue // 返回组件
BaseButton.vue // 按钮组件
BaseCard.vue // 卡片组件
BaseFullScreen.vue // 全屏组件
BaseInput.vue  // 输入框组件
BaseLoading.vue  // loading组件
BaseMask.vue // 遮罩组件
BaseSearchBox.vue  // 搜索框组件
BaseTextArea.vue // 多行输入框组件
BaseTitle.vue  // 标题组件
BreadCrumb.vue // 面包屑组件
ButtonBox.vue  // 按钮组组件
ButtonInverted.vue // 反色按钮组件
InputPassword.vue  // 密码框组件
InputWithTest.vue // 包含检测的输入框组件
LinkageSelector.vue // 联动选择组件

【功能组件】

  按照功能来设置目录,如下所示

弹出框(Alert)
类别管理(Category)
评论管理(Comment)
主页(Home)
点赞管理(Like)
文章管理(Post)
页面尺寸(Size)
公共头部(TheHeader) 用户管理(User)

 

整体思路

【全屏布局】

  使用设置高度的全屏布局方式,主要通过calc来实现

<div
  id="root"
  :class="$style.wrap"
  :style="{height:wrapHeight+"px"}"
>
  ...
  <TheHeader :class="$style.header"/>
  <main :class="$style.main">
    <transition :name="transitionName">
      <router-view :class="$style.router" />
    </transition>
  </main>
</div>
.header {
  height: 40px;
}
.main {
  position: relative;
  height: calc(100% - 40px);
  overflow: auto;
}

【层级管理】

  项目的层级z-index,只使用0-3

  全屏的弹出框优化级最高,设置为3;侧边栏设置为2;页面元素默认为0,如有需要,要设置为1

【全局弹出层】

  在入口文件App.vue中设置全局的弹出层和loading,所有组件都可以共用

// App.vue
<template>
  <div
    id="root"
    :class="$style.wrap"
    :style="{height:wrapHeight+"px"}"
  >
    <AlertWithLoading v-show="doShowLoading" />
    <AlertWithText
      v-show="alertText !== """
      :text="alertText"
      :onClick="() => {$store.commit(HIDE_ALERTTEXT)}"
    />
    <TheHeader :class="$style.header"/>
    <main :class="$style.main">
      <transition :name="transitionName">
        <router-view :class="$style.router" />
      </transition>
    </main>
  </div>
</template>

【路由管理】

  vue-router使用静态路由表的形式对路由进行管理,虽然没有react-router-dom灵活,但方便寻找,一目了然

  按路由设置按需加载组件,并设置滚动行为

import Vue from "vue"
import Router from "vue-router"

Vue.use(Router)
export default function createRouter() {
  return new Router({
    mode: "history",
    routes: [
      {
        path: "/",
        component: () => import(/* webpackChunkName:"home" */ "@/components/Home/Home"),
        name: "home",
        meta: { index: 0 }
      },
      {
        path: "/posts",
        component: () => import(/* webpackChunkName:"post" */ "@/components/Post/PostList"),
        name: "postlist"
      },
      {
        path: "/posts/search",
        component: () => import(/* webpackChunkName:"post" */ "@/components/Post/SearchPost"),
        name: "searchpost"
      },
      {
        path: "/posts/:postid",
        component: () => import(/* webpackChunkName:"post" */ "@/components/Post/Post"),
        name: "post",
        children: [
          {
            path: "comments",
            name: "commentlist",
            component: () => import(/* webpackChunkName:"comment" */ "@/components/Comment/CommentList"),
            children: [
              {
                path: "add",
                name: "addcomment",
                component: () => import(/* webpackChunkName:"comment" */ "@/components/Comment/AddComment")
              },
              {
                path: ":commentid/update",
                name: "updatecomment",
                component: () => import(/* webpackChunkName:"comment" */ "@/components/Comment/UpdateComment")
              },
              {
                path: ":commentid/delete",
                name: "deletecomment",
                component: () => import(/* webpackChunkName:"comment" */ "@/components/Comment/DeleteComment")
              }
            ]
          }
        ]
      },
      {
        path: "/categories",
        component: () => import(/* webpackChunkName:"category" */ "@/components/Category/CategoryList"),
        name: "categorylist"
      },
      {
        path: "/categories/:number",
        component: () => import(/* webpackChunkName:"category" */ "@/components/Category/Category"),
        name: "category"
      },
      {
        path: "/topics/:number",
        component: () => import(/* webpackChunkName:"category" */ "@/components/Category/CategoryTopic"),
        name: "topic"
      },
      // 注册
      {
        path: "/signup",
        component: () => import(/* webpackChunkName:"user" */ "@/components/User/AuthSignup"),
        name: "signup"
      },
      // 按手机号登录
      {
        path: "/signin_by_phonenumber",
        component: () => import(/* webpackChunkName:"user" */ "@/components/User/AuthSigninByPhoneNumber"),
        name: "signin_by_phonenumber"
      },
      // 按用户名登录
      {
        path: "/signin_by_username",
        component: () => import(/* webpackChunkName:"user" */ "@/components/User/AuthSigninByUsername"),
        name: "signin_by_username"
      },
      // 用户页面
      {
        path: "/users/:userid",
        component: () => import(/* webpackChunkName:"user" */ "@/components/User/UserDesk"),
        name: "user"
      }
    ],
    scrollBehavior(to, from, savedPosition) {
      if (savedPosition) {
        return savedPosition
      }
      return { x: 0, y: 0 }
    }
  })
}

【状态管理】

  每个组件的状态管理命名为module.js,保存在当前组件目录下

import Vue from "vue"
import Vuex from "vuex"
import auth from "@/components/User/module"
import alert from "@/components/Alert/module"
import post from "@/components/Post/module"
import category from "@/components/Category/module"
import like from "@/components/Like/module"
import size from "@/components/Size/module"
import comment from "@/components/Comment/module"

Vue.use(Vuex)
export default function createStore() {
  return new Vuex.Store({
    modules: {
      auth,
      alert,
      post,
      category,
      like,
      size,
      comment
    }
  })
}

  每个组件的状态包括state、getters、actions和mutations字段,以Category组件为例

import { BASE_CATEGORY_URL } from "@/constants/API"
import { getNumberWithoutPostPositiveZero, getCategoryNumbers } from "@/utils/util"

export const LOAD_CATEGORIES = "LOAD_CATEGORIES"
export const LOAD_CATEGORIES_ASYNC = "LOAD_CATEGORIES_ASYNC"
const category = {
  state: {
    docs: []
  },
  getters: {
    categoryCount: state => state.docs.length,
    getCategoriesByNumber: state => state.docs.reduce((obj, t) => {
      obj[t.number] = t
      return obj
    }, {}),
    getCategoryByNumber: state => number => state.docs.find(doc => doc.number === number),
    getPosterityCategories: (state, getters) => number => {
      const reg = new RegExp(`^${getNumberWithoutPostPositiveZero(number)}`)
      return state.docs.filter(doc => {
        doc.titleDatas = getCategoryNumbers(doc.number).map(t => getters.getCategoriesByNumber[t].name)
        return String(doc.number).match(reg) && (doc.posts.length)
      })
    },
    getChildrenCategoryies: state => number => {
      const reference = String(getNumberWithoutPostPositiveZero(number))
      const len = reference.length
      const regExp = new RegExp(`^${reference}(0[1-9]|[1-9][0-9])(0){${8 - len}}`)
      return state.docs.filter(doc => String(doc.number).match(regExp))
    },
    getCategoryRootDatas: state => state.docs.filter(doc => Number(String(doc.number).slice(2)) === 0),
    getRecommendedCategories: state => state.docs.filter(t => t.recommend).sort((a, b) => a.index - b.index)
  },
  actions: {
    /* 获取全部类别信息 */
    [LOAD_CATEGORIES_ASYNC]({ commit }) {
      return new Promise((resolve, reject) => {
        this._vm.$axios({
          commit,
          url: BASE_CATEGORY_URL,
          doHideAlert: true,
          success(result) {
            // 保存类别
            commit(LOAD_CATEGORIES, result.docs)
            // 向前端通知操作成功
            resolve(result.docs)
          },
          fail(err) {
            // 向前端通知操作失败
            reject(err)
          }
        })
      })
    }
  },
  mutations: {
    /* 保存类别信息 */
    [LOAD_CATEGORIES](state, payload) {
      state.docs = payload
    }
  }
}
export default category

【数据传递】

  组件间的数据传递方式一般有三种,一种是使用vue中的props和自定义事件,另一种是使用路由的params属性,还有一种是通过vuex

  1、props和自定义事件

// BaseInput
<template>
  <input
    :class="$style.input"
    :value="value"
    autocomplete="off"
    autocapitalize="off"
    @input="$emit("input", $event.target.value)"
  >
</template>
<script>
export default {
  props: {
    value: { type: String, default: "" }
  }
}
</script>

// InputPassword
<input
  :class="$style.input"
  :placeholder="placeholder"
  :value="value"
  autocomplete="off"
  autocapitalize="off"
  type="password"
  @input="$emit("input",$event.target.value)"
>

  2、路由的params属性

// Post.vue
 <BaseBack @click.native="$router.push($route.params.parentPath || "/")">返回</BaseBack>

//AuthSign.vue
<template>
    <router-link
        :active-class="$style.active"
        :to="{ name: "signin", params: { parentPath } }"
    >登&nbsp;录</router-link>
</template>
<script>
export default {
  computed: {
    parentPath() {
      const temp = this.$route.params.parentPath
      if (temp) {
        return temp
      }
      return ""
    }
  }
}
</script>

  3、使用vuex

// Category.vue
<template>
  <article v-if="category" :class="$style.box">
    <BaseBack @click.native="$router.push("/categories")">类别列表</BaseBack>
    <BaseTitle>{{ category.name }}知识体系</BaseTitle>
    ...
  </article>
</template>
<script>
export default {
  computed: {
    category() {
      return this.$store.getters.getCategoryByNumber(Number(this.paramsNumber))
    }
    ...
  }
}
</script>

 

项目优化

【离线缓存】

  通过service worker实现离线缓存效果

const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin")

plugins: [
  new SWPrecacheWebpackPlugin({
    dontCacheBustUrlsMatching: /.w{8}./,
    filename: "service-worker.js",
    logger(message) {
      if (message.indexOf("Total precache size is") === 0) {
        return;
      }
      if (message.indexOf("Skipping static resource") === 0) {
        return;
      }
      console.log(message);
    },
    navigateFallback: "https://www.xiaohuochai.cc",
    minify: true,
    navigateFallbackWhitelist: [/^(?!/__).*/],
    dontCacheBustUrlsMatching: /./,
    staticFileGlobsIgnorePatterns: [/.map$/, /.json$/],
    runtimeCaching: [{
        urlPattern: "/",
        handler: "networkFirst"
      },
      {
        urlPattern: //(posts|categories|users|likes|comments)/,
        handler: "networkFirst"
      }
    ]
  })
]

【添加到桌面】

  andriod下,通过设置manifest.json文件添加到桌面,而IOS则需要设置meta标签

<meta name="theme-color" content="#fff"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="前端小站">
<link rel="apple-touch-icon" href="/logo/logo_256.png">
<link rel="shortcut icon" href="/logo/favicon.ico">
<link rel="manifest" href="/manifest.json" />

// manifest.json
{
  "name": "小火柴的前端小站",
  "short_name": "前端小站",
  "start_url": "/",
  "display": "standalone",
  "description": "",
  "theme_color": "#fff",
  "background_color": "#d8d8d8",
  "icons": [{
      "src": "./logo/logo_32.png",
      "sizes": "32x32",
      "type": "image/png"
    },
    {
      "src": "./logo/logo_48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "./logo/logo_96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "./logo/logo_144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "./logo/logo_192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./logo/logo_256.png",
      "sizes": "256x256",
      "type": "image/png"
    }
  ]
}

【子页面刷新】

  子页面刷新时,可能会出现得不到从父级传递过来的数据的情况,笔者的处理是跳转到父级页面

mounted() {
  if (!this.comment && this.operate === "update") {
    this.$router.push(`/posts/${this.postId}/comments`)
  } else {
    this.setTextAreaValue()
  }
}

【promise】

  为actions添加Promise,方便状态改变后的处理

[LOAD_COMMENTS_ASYNC]({ commit }, payload) {
  return new Promise((resolve, reject) => {
    this._vm.$axios({
      commit,
      data: payload,
      url: BASE_COMMENT_URL,
      doHideAlert: true,
      success(result) {
        // 保存类别
        commit(LOAD_COMMENTS, result.docs)
        // 向前端通知操作成功
        resolve(result.docs)
      },
      fail(err) {
        // 向前端通知操作失败
        reject(err)
      }
    })
  })
}

【组件共用】

  由于编辑和新建组件用到的元素是一样的,只不过,新建组件时内容为空,编辑组件时需要添加内容,这时就可以复用组件

// AddComment.vue
<CommentForm operate="add" />

//UpdateComment.vue
<CommentForm operate="update" />

【清理环境】

  如果使用addEventListener绑定了事件处理函数,在组件销毁的时候,要及时清理环境

mounted() {
  window.addEventListener("devicemotion", throttle(this.testShake))
}
beforeDestroy() {
  window.removeEventListener("devicemotion", throttle(this.testShake))
}

【应用和数据分离】

  使用配置数据,实现数据和应用分离,配置数据主要是API调用地址,以常量的形式存储在constants目录下

// API.js
let API_HOSTNAME
if (process.env.NODE_ENV === "production") {
  API_HOSTNAME = "https://api.xiaohuochai.cc"
} else {
  API_HOSTNAME = "/api"
}
export const SIGNUP_URL = `${API_HOSTNAME}/auth/signup`
export const SIGNIN_BYUSERNAME_URL = `${API_HOSTNAME}/auth/signin_by_username`
export const SIGNIN_BYPHONENUMBER_URL = `${API_HOSTNAME}/auth/signin_by_phonenumber`
export const VERIFICATE_URL = `${API_HOSTNAME}/auth/verificate`

export const BASE_USER_URL = `${API_HOSTNAME}/users`
export const BASE_POST_URL = `${API_HOSTNAME}/posts`
export const BASE_TOPIC_URL = `${API_HOSTNAME}/topics`
export const BASE_CATEGORY_URL = `${API_HOSTNAME}/categories`
export const BASE_LIKE_URL = `${API_HOSTNAME}/likes`
export const BASE_COMMENT_URL = `${API_HOSTNAME}/comments`

export const ADMIN_URL = "https://admin.xiaohuochai.cc"

【函数节流】

  为触发频率较高的函数使用函数节流

/**
 * 函数节流
 * @param {fn} function test(){}
 * @return {fn} function test(){}
 */
export const throttle = (fn, wait = 100) => function func(...args) {
  if (fn.timer) return
  fn.timer = setTimeout(() => {
    fn.apply(this, args)
    fn.timer = null
  }, wait)
}

【DNS预解析】

  DNS预解析通过设置meta标签实现

<link rel="dns-prefetch" href="//api.xiaohuochai.cc" />
<link rel="dns-prefetch" href="//static.xiaohuochai.site" />
<link rel="dns-prefetch" href="//demo.xiaohuochai.site" />
<link rel="dns-prefetch" href="//pic.xiaohuochai.site" />

【图片懒加载和webp】

  通过vue-lazyload插件实现图片懒加载和andriod系统下图片转换成webp格式

Vue.use(VueLazyload, {
  loading: require("./assets/imgs/loading.gif"),
  listenEvents: ["scroll"],
  filter: {
    webp(listener, options) {
      if (!options.supportWebp) return
      const isCDN = /xiaohuochai.site/
      if

当前文章:http://0477auto.com/pgz7em58eq/index.html

发布时间:2018-12-12 01:13:36

赚赚直播下载 起步赚钱网 基调网络挂机赚钱 梦幻西游生活技能赚钱 朋友圈人好多,想做个挣钱的项目 褥羊毛群 家教网兼职常青藤 兼职小投资

编辑:安文

我要说两句: (0人参与)

发布