Vue全家桶+koa2+MySql(sequelize)重构“零食商贩”项目
前言
原项目使用微信小程序配合ThinkPHP5.0打造的微信小程序商城,作为一名“中端工程师”来讲能够使用javascript前后一起梭也是工作之余非常愉快的事情。所以如果你也想梭一把,那么可以继续往下看。
项目不足
1.前端部分没有100%还原设计稿,因为压根就没有设计稿。唯一的标准就是自己的“像素眼”。
2.作为WEB项目自然而然就阉割了微信的登录与支付体系。当然登录体系也许会加上。
3.后端部分没有做严格的容错处理,这里更多的是提供一些问题的解决方法和分享自己遇到的坑儿。
项目运行效果
前端部分
前端部分好像没有什么好说的了,网上vue全家桶项目一搜一大把。这里主要分享一下从前端角度如何分解产品设计稿以及css模块化的处理。本篇文章将重点放在服务端上。
分解一款产品
不知道其他小伙伴拿到设计稿是如何开始的,记得才开始写前端的时候也是根据设计稿从上到下,从左到右一步一步实现。不过往往这样的开发流程遇到大的需求变更,或是产品同学提不切实际需求的时候是非常头痛的。这里提一句,前端小伙伴一定要多多参加产品需求会,一方面可以增加对公司业务的理解,另一方面可以把产品同学不切实际的需求扼杀在摇篮中,防止拍脑袋决策出现。
分解设计稿
“零食商贩”项目虽然页面有多个,但是我们分解设计稿就会发现其实该项目由这几个部分组成。
Header与Footer
这一部分称为公共部分。基本上每个页面都由header、footer以及中间主要内容组成。
layout
这一部分称为“基模块”。也就是页面大部分都是由该模块组合而成,无非是一些内容的增减,实际开发中完全可以通过数据以及css达到各个页面个性化定制需求。
以上三个页面都一样,只是换了马甲。
结构抽象出来应该就是这样:
//vue模板 <template> <div> <!-- 小标题 --> <p>{{ title }}</p> <slot></slot> <div> <!-- 产品模块 --> <div> <img src="" alt="" > <p >{{ item.name }}</p> <p >{{ item.price }}</p> </div> </div> </div> </template>
css模块化
虽然vue提供了scope来方便编写组件内部的css,防止css名相互污染。但有时候scope造成的作用域问题不方便调试。所以这里采用了同样流行的CSS Modules。
开启方式也很简单,如果是使用vue-cli方式构建的vue项目,只需要两步即可开启:
进入文件夹build/vue-loader-conf.js
module.exports = { // css模块化 cssModules: { // 通过给类名加入唯一前缀防止类名冲突 localIdentName: '[name]---[local]---[hash:base64:5]', camelCase: true } }
在每个组件中申明css modules并使用
<template> //$style部分将会替换为'[name]---[local]---[hash:base64:5]' <div :class="$style.header"></div> </template> <style lang='scss' module> .header{ color:blue } </style>
css modules 的核心原理就是通过加入唯一的class类名从而防止css类名冲突。本质上的效果与scope是一样的。
一款“异常简陋”的轮子
项目写到一半,才发现需要一款符合微信官方风格的UI组件,虽然官方UI组件颜值上并不高。但是为了视觉上的统一,又苦于网上没有找到过于简陋的UI组件,所以自己封装了一个。目前只有picker组件和基于picker组件的地址选择组件。
npm 命令直接安装,本项目默认是安装好了的。因为简陋所以也就不提供什么文档了...... 具体用法可以看项目内部实现。
npm install only-ui --save
后端部分
项目后端部分采用koa2搭配mysql数据实现,koa2官网是这样介绍的:
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
正是因为koa2轻量,没有一些官方性的约束,你可以很方便搭建自己的前端项目。但同时也带来了一些问题,“一千个人就有一千种koa MVC的写法”,所以如果是大一点项目或是团队项目还是比较推荐egg.js来编写。
项目结构
- server:根目录
- api:存放的是我们项目的数据接口
- database:数据库脚本文件,可以直接导入到navicate中使用
- dbs:数据库配置以及数据模型(sequelize)
- public:一些资源图片就存放在这里
- view:视图模板(本项目不需要)
- app.js:koa主文件
- index.js入口文件
初始化项目
//通过koa-generator快速搭建koa2服务 npm install -g koa-generator //创建项目 输入项目名称 koa2 -e [项目名称] //安装依赖 npm install
这样我们基本上创建了一个比较简单的koa2项目,我们来看一下现在已经安装了哪些依赖
"dependencies": { "debug": "^2.6.3", "ejs": "~2.3.3", //ejs模板,因为我们创建项目的时候用的ejs "koa": "^2.2.0", "koa-bodyparser": "^3.2.0", //解析request "koa-convert": "^1.2.0", "koa-json": "^2.0.2", //格式化json数据 "koa-logger": "^2.0.1", //系统日志 "koa-onerror": "^1.2.1", //错误处理 "koa-router": "^7.1.1", //路由 "koa-static": "^3.0.0", //静态资源处理 "koa-views": "^5.2.1"//模板渲染 }, "devDependencies": { "nodemon": "^1.8.1" //可以随时监听服务端文件改动,并更新 }
前面说了,koa就像一块电脑主板一样,需要什么东西自己可以往上面加。这里有更多中间件,如果还是没有你需要的,你完全可以自己写一个造福社区。
只有以上的中间件还是不够的,比如我并不希望使用require语法导入模块所以我们换成 es6 modules方式导入。这里还需要安装:
"babel-core": "^6.26.3", "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.24.1", "babel-register": "^6.26.0",
同时修改项目结构
//index.js // 启动文件 require('babel-register') ({ 'presets':['env'] }) require('./app.js')
这样我们就可以在主文件中使用import导入我们需要的模块。(中间件的导入在在项目中有清晰的注释)
我们来尝试启动一下koa2服务,启动之前要修改一下npm(你怕吗) script
"scripts": { "start": "nodemon index.js", //使用nodemon 启动index文件 },
数据库
服务端开发怎么能少了数据库,不知道是不是错觉,koa项目好像更多的是配合mongodb来使用。本项目使用mysql完全是因为自己的习惯,再一个使用mongodb处理复杂一点的数据表间关系确实有点头痛。。。
我们在这里引入sequelize来操作mysql,毕竟使用原生sql显得不是那么优雅!什么是sequelize?
Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, SQLite 和 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能.
通俗一点说,就好比Java中的hibernate,mongodb中的mongoose。让我们以面向对象的方式操作数据库。
现在让我们来安装sequelize。mysql的安装
1. 安装sequelize
// 安装sequelize $ npm install --save sequelize // 安装驱动 $ npm install --save mysql2
2. 配置sequelize
既然我们使用sequelize操作数据库,那么一番基本的配置一定是要有的。
//config.js // sequelize配置文件 export default { // 数据库名称 database: '', // 用户名 username: '', // 密码 password: '', // 地址 host: '127.0.0.1', // 使用什么数据库 dialect: 'mysql', // 连接池 pool: { max: 5, min: 0, acquire: 30000, idle: 10000 }, // 数据表全局配置 define:{ //是否冻结表名,最好设置为true,要不sequelize会自动给表名加上复数s造成查询数据失败。 //mongoose也有这样的问题... freezeTableName:true, // 是否为表添加 createdAt 和 updatedAt 字段 // createdAt 记录表的创建时间 // updatedAt 记录字段更新时间 timestamps:false, // 是否为表添加 deletedAt 字段 // 在日常开发中删除数据记录是一大禁忌,因此我们删除数据并不会真正删除,而是为他添加 // deletedAt字段 paranoid:false, //是否开启op operatorsAliases: false }, // 时区 timezone: '+08:00' }
如此一番操作,sequelize已经与mysql建立起联系,但还无法工作,我们需要给数据表建立模型。建立模型之前我们导入刚刚已经配置好的文件。如下图
index.js中导入我们的配置config.js文件,其他的文件都是模型(可以把它理解为数据库中的表,让我们更好操作它)。
3. 定义模型
我们在navicat中看到的表长这个样子
我们的模型长这个样子:
//banner.js // banner 模型 export default (sequelize, DataTypes) => { //这里的banner为你的数据表名 return sequelize.define('banner', { id: { //定义类型 type: DataTypes.INTEGER(), //主键 primaryKey: true }, productsId: { //定义类型 type: DataTypes.INTEGER(), }, img_id: { //定义类型 type: DataTypes.INTEGER(), } }) }
然后导入到index.js中统一管理(一定要让你的所有模型在同一个sequelize实例下,曾经这个问题困扰了我很久。。。)
//index.js import Sequelize from 'sequelize' import config from '../config.js' // 实例化sequelize export const sequelize = new Sequelize(config) // 导入模型统一管理(推荐使用官方方法) export const Banner = sequelize.import(__dirname + '/banners.js')
4. 建立表与表之间关系
表与表之间无外乎:
一对一 belongsto
外键一对一 hasone
一对多 hasmany
多对多 belongsToMany
拿我们项目中的banner与image来举例,banner指向唯一image,image对应唯一banner那么他们之间关系就为一对一。
//定义关系 Banner.belongsTo(Image, { foreignKey: 'img_id', targetKey: 'id' })
现在我们建立了模型也定义了模型间关系,现在我们开始来使用。
5.为前端提供接口
还是以banner为例
//api/banner.js // banner接口 import Router from 'koa-router' // 引入用户模型 import { Banner,Image } from '../dbs/models/index.js' //定义接口前缀 let router = new Router({ prefix:'/banner' }) //暴露给前端的接口 router.get('/', async (ctx,next)=>{ let banner = await Banner.findAll({ //声明要包含的模型,之前声明的关系将在这里发挥作用 include:[{ model:Image }], //过滤不需要的数据 attributes:{ exclude:['img_id'] } }) //最终返回的数据 ctx.body = { banner } }) export default router
最后我们需要把路由导入到主文件中。
//app.js //引入 import banner from './api/banner.js' //使用 app.use(banner.routes()).use(banner.allowedMethods())
现在你可以在前端通过axios访问你的数据接口了,我们看一下最终执行效果。
sequelize已经自动帮我们生成了sql语句:
postman中的数据:
其他更详细的内容可以查看mini-shop!