运行图
安装
npx create-nuxt-app project
配置
koa + axios
typescript-vuex github
ui框架
element-ui
目录结构
assets ---资源目录
layouts ---布局目录
middleware ---中间件目录
plugins ---插件目录
static ---静态(后台)
- 在您的 vue 模板中, 如果你需要引入 assets 或者 static 目录, 使用 ~/assets/your_image.png 和 ~/static/your_image.png方式。
异步数据 SSR解析
- 页面数据 asyncData
先请求
扔个模板结构(静态渲染) asyncData(请求拿数据)
把编译的结果扔给客户端 服务器下发一个script 挂载到window下
同步到浏览器(交互) 虚拟编译和服务器扔过来的作对比, 不同重新请求
第一参数: 当前页面的上下文对象
- ts中操作
@Component({
async asyncData({params,app,$axios}) {
console.log(params,app);
app.store.dispatch('search/setName', params.key)
return {
keysword: params.key
}
},
components: {
ECrumb
},
})
- vuex fetch
nuxtServerInit
- 第一次请求
- 保存用户登录
- 全局数据
==如果你使用状态树模块化的模式,只有主模块(即 store/index.js)适用设置该方法(其他模块设置了也不会被调用)。==
layouts 页面模板
[图片上传失败...(image-ac40cb-1562038525452)]
pages 即是路由
- 基础路由
- 动态路由
- 嵌套路由
ts中
npm i @nuxt/typescript -D
npm i vue-class@0.3.1 vue-property-decorator@7 -S
- tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"esnext",
"esnext.asynciterable",
"dom"
],
"esModuleInterop": true,
"experimentalDecorators": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noImplicitAny": false,
"noEmit": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
"types": [
"@types/node",
"@nuxt/vue-app"
]
}
}
head layout asyncData...等 放在@Component使用
@Component({
//
head() {
return {
title: this.name
}
},
layout: 'search'
})
.vue中ts 独立出来 才能引入单独ts
- vuex 使用
index.js
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
import * as search from './module/search'
import geo from './module/geo'
// webpack 中 生产模式或开发
const createStore = () => {
return new Vuex.Store({
state,
getters,
mutations,
actions,
modules: {
[search.name]: search,
geo
}
})
}
export default createStore
- 相关模块 module/geo.ts
import { RootStateTypes } from '../types';
import { MutationTree, ActionTree } from 'vuex';
const namespaced = true;
interface stateInterface {
city: string;
}
const state: stateInterface = {
city: ''
};
export const types = {
CITY: 'CITY'
};
const mutations: MutationTree<stateInterface> = {
[types.CITY]: (state, city: string) => {
state.city = city;
}
};
const actions: ActionTree<stateInterface, RootStateTypes> = {
setCity({ commit }, city) {
commit(types.CITY, city);
}
};
export default { namespaced, state, actions, mutations };
- 如何拓展 webpack 配置 --- 添加 alias 配置
背景:给 utils 目录添加别名
刚刚说到,Nuxt.js内置了 webpack 配置,如果想要拓展配置,可以在 nuxt.config.js 文件中添加。同时也可以在该文件中,将配置信息打印出来。
extend (config, ctx) {
console.log('webpack config:', config)
if (ctx.isClient) {
// 添加 alias 配置
config.resolve.alias['~src'] = __dirname
config.resolve.alias['~utils'] = path.join(__dirname, 'utils')
}
}
支持es6语法
安装
yarn add babel-cli babel-core babel-preset-es2015 babel-preset-stage-0
修改package.json文件,在“dev”和“start”命令后面新增:--exec babel-node
项目根目录下新增babel配置文件“.babelrc”文件,写入以下配置
{
"preset": ["es2015","stage-0"]
}
serve 相关
serve目录
Passport 解决登陆认证的问题
Web应用一般有2种登陆认证的形式:
这次项目实现用户名和密码认证登陆
基于本地 配置策略 进行 用户名和密码验证
passport.js
// 身份验证
// http://blog.fens.me/nodejs-express-passport/ 解决登陆认证的问题
import passport from 'koa-passport'
import LocalStrategy from 'passport-local'
// 用户表
import UserModel from '../../dbs/models/user'
// 配置策略. (具体操作)
passport.use(new LocalStrategy(async function (username, password, done) {
let result = await UserModel.findOne({
username: username
})
// 存在
if (result != null) {
// 密码对不对
if (result.password === password) {
return done(null, result)
} else {
return done(null, false, {
msg: '密码错误'
})
}
} else {
// 不存在
return done(null, false, {
msg: '用户不存在'
})
}
}))
// 保存用户
passport.serializeUser(function (user, done) {
done(null, user)
})
// 删除用户
passport.deserializeUser(function (user, done) {
done(null, user)
})
export default passport
路由控制 user.js
- 登录
- 注册
- 验证验证码 (发送验证码) npm i nodemailer@4.6.8
- 退出
相关资料
ctx.session.passport.user 用户信息
ctx.request.body post传参
ctx.params ctx.query get传参
import Router from 'koa-router';
// 使用redis 验证 --- 不同用户同时发送验证码 区分不用户,不能存表(量大,内存会溢出),
import Redis from 'koa-redis';
// 给用户发邮件
import nodeMailer from 'nodemailer';
import Email from '../dbs/config';
import userModel from '../dbs/models/user';
import axios from './utils/axios';
import passport from './utils/passport';
const router = new Router();
const client = new Redis({}).client;
function err(msg: string) {
return {
code: -1,
msg
};
}
// 注册
router.post('/signup', async (ctx: any) => {
// ctx.request.body post传参
const { username, password, email, code } = ctx.request.body;
// 验证code
if (code) {
// 获取对应的code 验证码
const saveCode = await client.hget(`nodemail:${username}`, 'code');
// 过去时间
const expire = await client.hget(`nodemail:${username}`, 'expire');
if (code === saveCode) {
// 是否过期
if (Date.now() - expire > 0) {
ctx.body = {
code: -1,
msg: '验证码已过期,请重新验证'
};
return false;
}
} else {
// 验证码错误
ctx.body = {
code: -1,
msg: '验证码错误'
};
}
} else {
ctx.body = {
code: -1,
msg: '验证码不能为空'
};
}
// 验证用户是否被注册过.
try {
await userModel.findOne(username);
ctx.body = err('用户名被注册过了');
} catch {
let user = userModel.create({
username,
password,
email
});
if (user) {
// 注册后自动登录
let res = await axios.post('/signin', { username, password });
if (res.data && res.data.code === 0) {
ctx.body = {
code: 0,
data: res.data.user,
msg: '注册成功'
};
} else {
ctx.body = err('error');
}
} else {
// 创建失败
ctx.body = err('注册失败');
}
}
});
// 登录
router.post('/signin', (ctx: any, next: any) => {
// 登录 验证
return passport.authenticate(`local`, function(
error: any,
user: any,
info: any
) {
if (error) {
ctx.body = err(error);
return false;
}
if (user) {
ctx.body = {
code: 0,
msg: '登录成功',
data: user
};
// passport 登录用户初始化session
return ctx.login(user);
} else {
ctx.body = {
code: 1,
msg: info
};
}
})(ctx, next);
});
// 验证
router.post('/verify',async (ctx: any,next: any) => {
let {username,email} = ctx.request.body
// 阻止频繁访问
let expire = await client.hget(`nodemail:${username}`, 'expire');
if(expire && (Date.now() - expire) < 0) {
ctx.body = {
code: -1,
msg: '请求过于频繁'
}
return false
}
// 邮件配置
let transporter = nodeMailer.createTransport({
host: Email.smtp.host,
post: Email.smtp.port,
// 监听其他端口(原: 465)
secure: false,
auth: {
user: Email.smtp.user,
// 授权码
pass: Email.smtp.pass
}
})
// 新建一个验证码信息
let ko = {
code: Email.code(),
expire: Email.expire(),
user: username,
email: email,
}
// 邮件信息配置
let mailOptions = {
from: `认证邮件<${Email.smtp.user}>`,
to: ko.email,
// 标题
subject: `网站的注册码`,
// 发送的text或者html格式
html: `你的验证码是${ko.code}`
}
// 发送
await transporter.sendMail(mailOptions, (error,info) => {
if(error) {
return console.log(error)
}
// hmset 为散列里面的一个或多个键设置值 OK hmset('hash-key', obj)
client.hmset(`nodemail:${ko.user}`, ko)
})
ctx.body = {
code: 0,
msg: `验证码已发送, 有效期1min`
}
})
router.post(`/exit`, async (ctx,next) => {
// passport 删除该用户session
await ctx.logout()
// 二次验证是否退出 passport的验证
// isAuthenticated: 测试该用户是否存在于session中(即是否已登录)
if(ctx.isAuthenticated()) {
ctx.body = err('退出失败')
}else{
ctx.body = {
code: 0
}
}
})
// 获取用户信息
router.get('/user', async (ctx) => {
if(ctx.isAuthenticated()) {
let {username,email} = ctx.session.passport.user
ctx.body = {
code: 0,
user: username,
email
}
}else{
ctx.body = {
code: -1,
user: '',
email: ''
}
}
})
export default router
app.js
npm install koa-bodyparser koa-generic-session koa-json koa-passport passport-local
// 引入mongoose redis
import mongoose from 'mongoose'
// 处理passport相关请求
import bodyParser from 'koa-bodyparser'
// session删写
import session from 'koa-generic-session'
import Redis from 'koa-redis'
// 代码格式化. 打印.
import json from 'koa-json'
import dbsConfig from './dbs/config'
import Passpot from './interface/utils/passport'
import UserInterface from './interface/user'
import passport from './interface/utils/passport';
// session加密处理的两字符
app.keys = ['keys','key']
app.proxy = true
// 存储
app.use(session({
key: 'egg-mt',
prefix: 'mt:uid',
store: new Redis()
}))
app.use(bodyParser({
enbleTypes: ['text','json','form']
}))
app.use(json())
// 连接数据库
mongoose.connect(dbsConfig.dbs, {
useNewUrlParser: true
})
app.use(passport.initialize())
app.use(passport.session())
// 添加路由
密码加密
crypto-js (加密算法类库)
ts识别全局方法/变量
shims-vue.d.ts
import VueRouter, { Route } from "vue-router";
import Vue from 'vue';
declare var document: Document;
declare module '*.vue' {
export default Vue;
}
declare module "*.ts" {
const value: any;
export default value;
}
declare global {
interface window {
require: any;
}
}
// 识别 this.$route
declare module 'vue/types/vue' {
interface Vue {
$router: VueRouter; // 这表示this下有这个东西
$route: Route;
$notify: any;
}
}
- this 的类型检查
在根目录的 tsconfig.json 里面加上 "noImplicitThis": false ,忽略 this 的类型检查
"noImplicitThis": false,
插件
js-pinyin
地图的使用
地图组件
- 添加点标记, 文本标签
- 添加点击事件
map.vue
<template>
<div>
<div :id="id"
:class='["m-map", {fixed: fixed}]'
:style="{width:width+'px',height:height+'px',margin:'34px auto' }" ref='map'></div>
<transition name="fade">
<div class="model"
v-show='show'
@click="show = false">
<div :id='"max-"+id'
class="fixed-map"
:style="{width:mapWidth+'px',height:mapHeight+'px'}">
</div>
</div>
</transition>
</div>
</template>
<script lang='ts'>
import { Component, Vue, Prop } from "vue-property-decorator";
declare var window: any;
declare var AMap: any;
@Component({
props: {
id: {
type: String,
default: "map"
},
// 点标记
// markerList: {
// type: Array,
// default() {
// return [{
// name: '天安门',
// location: [116.39, 39.9],
// add: '北京'
// }]
// }
// },
width: Number,
height: Number,
fixed: Boolean
}
})
export default class Map extends Vue {
key: string = "12ef08e92a0ce0963b4698a73de243bc";
map: any = null;
mapWidth: number = 0;
mapHeight: number = 0;
show: boolean = false;
@Prop({
type: Array,
default() {
return [
{
name: "天安门",
location: [116.39, 39.9],
add: "北京"
}
];
}
})
markerList: any[];
mounted() {
let that: any = this;
window.onMapLoad = () => {
// that.map = new AMap.Map(that.id, {
// resizeEnable: true,
// zoom: 11,
// center: that.markerList[0].location
// });
// AMap.plugin(
// [
// "AMap.ToolBar",
// "AMap.Scale",
// "AMap.OverView",
// "AMap.MapType",
// "AMap.Geolocation"
// ],
// function() {
// // 在图面添加工具条控件,工具条控件集成了缩放、平移、定位等功能按钮在内的组合控件
// that.map.addControl(new AMap.ToolBar());
// // 在图面添加比例尺控件,展示地图在当前层级和纬度下的比例尺
// // map.addControl(new AMap.Scale());
// // 在图面添加鹰眼控件,在地图右下角显示地图的缩略图
// // map.addControl(new AMap.OverView({ isOpen: true }));
// // 在图面添加类别切换控件,实现默认图层与卫星图、实施交通图层之间切换的控制
// // map.addControl(new AMap.MapType());
// // 在图面添加定位控件,用来获取和展示用户主机所在的经纬度位置
// that.map.addControl(new AMap.Geolocation());
// }
// );
// that.addMarker();
// mini
that.mapInit()
// normal
that.mapInit(`max-${that.id}`,`max-${that.id}`)
// let marker = new AMap.Marker({
// icon:
// "//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png",
// position: that.map.getCenter(),
// offset: new AMap.Pixel(-13, -30)
// });
// marker.setLabel({
// offset: new AMap.Pixel(0, -5), //设置文本标注偏移量
// content: "<div class='info'>1</div>", //设置文本标注内容
// direction: "center" //设置文本标注方位
// });
// that.add(marker);
};
var url = `https://webapi.amap.com/maps?v=1.4.14&key=${
this.key
}&callback=onMapLoad`;
var jsapi = document.createElement("script");
jsapi.charset = "utf-8";
jsapi.src = url;
document.head.appendChild(jsapi);
}
mapInit(id = 'map',name = 'map') {
let that: any = this;
that[name] = new AMap.Map(id, {
resizeEnable: true,
zoom: 11,
center: that.markerList[0].location
});
AMap.plugin(
[
"AMap.ToolBar",
"AMap.Scale",
"AMap.OverView",
"AMap.MapType",
"AMap.Geolocation"
],
function() {
// 在图面添加工具条控件,工具条控件集成了缩放、平移、定位等功能按钮在内的组合控件
that[name].addControl(new AMap.ToolBar());
// 在图面添加比例尺控件,展示地图在当前层级和纬度下的比例尺
// map.addControl(new AMap.Scale());
// 在图面添加鹰眼控件,在地图右下角显示地图的缩略图
// map.addControl(new AMap.OverView({ isOpen: true }));
// 在图面添加类别切换控件,实现默认图层与卫星图、实施交通图层之间切换的控制
// map.addControl(new AMap.MapType());
// 在图面添加定位控件,用来获取和展示用户主机所在的经纬度位置
that[name].addControl(new AMap.Geolocation());
}
);
that.addMarker(name);
// mini打开大的
if(name='map') {
that[name].on('click', (e) => {
that.mapWidth = (window.innerWidth / 2) > 1100 ? (window.innerWidth / 2) : 1100
that.mapHeight = window.innerHeight * 0.85
that.show = true
})
}
}
addMarker(name = 'map') {
let map = this[name];
this.markerList.forEach((item, index) => {
// 点标记
let marker = new AMap.Marker({
icon:
"//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png",
position: item.location,
offset: new AMap.Pixel(-13, -30)
});
// 设置鼠标划过点标记显示的文字提示
marker.setTitle(item.add);
// 设置label标签
marker.setLabel({
offset: new AMap.Pixel(0, -5), //设置文本标注偏移量
content: `<div class='info'>${index + 1}</div>`, //设置文本标注内容
direction: "center" //设置文本标注方位
});
// 设置点击事件
marker.on("click", function(e) {
// 阻止冒泡
e.stopPropagation ? e.stopPropagation() :
e.cancelBubble = true
// 纯文本标记
let label = new AMap.Text({
offset: new AMap.Pixel(0, -30),
text: item.name,
anchor: "top", // 设置文本标记锚点
draggable: true,
cursor: "pointer",
angle: 0,
style: {
padding: ".25rem .75rem",
"margin-bottom": "1rem",
"border-radius": ".25rem",
"background-color": "white",
"border-width": 0,
"box-shadow": "0 2px 6px 0 rgba(114, 124, 245, .5)",
"text-align": "center",
"font-size": "14px"
// color: "blue"
},
position: item.location
});
map.add(label);
});
map.add(marker);
});
}
}
</script>
<style lang='scss'>
.amap-icon img {
width: 25px;
height: 34px;
}
.amap-marker-label {
border: 0;
background-color: transparent;
}
.info {
position: relative;
top: 0;
right: 0;
min-width: 0;
border-radius: 50%;
background-color: transparent;
color: #fff;
}
.info_text {
position: relative;
top: 0;
right: 0;
min-width: 0;
background: #fff;
box-shadow: 1px 1px 5px #999;
}
.model {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
.fixed-map {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%,-50%, 0);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
.fixed {
position: fixed !important;
top: 0;
overflow: hidden;
margin: 0 10px !important;
}
</style>
另一种显示方式: 标注图层
单条数据格式
console.log(JSON.stringify(LabelsData[6]))
{
"name": "京味斋烤鸭店",
"position": [116.462483, 39.992492],
"zooms": [10, 20],
"opacity": 1,
"zIndex": 4,
"icon": {
"type": "image",
"image": "https://a.amap.com/jsapi_demos/static/images/poi-marker.png",
"clipOrigin": [547, 92],
"clipSize": [50, 68],
"size": [25, 34],
"anchor": "bottom-center",
"angel": 0,
"retina": true
},
"text": {
"content": "京味斋烤鸭店",
"direction": "top",
"offset": [0, 0],
"style": {
"fontSize": 15,
"fontWeight": "normal",
"fillColor": "#666",
"strokeColor": "#fff",
"strokeWidth": 1
}
},
"extData": {
"index": 6
}
}
滚动事件
// 滚动距离
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
mounted() {
window.addEventListener("scroll", this.handleScroll, true); // 监听(绑定)滚轮滚动事件
}
// 滚动事件
handleScroll(e) {
}
destroyed() {
window.removeEventListener("scroll", this.handleScroll); // 监听(绑定)滚轮滚动事件
}