Dongsj的博客

Talk is cheap, show me the code.


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

【抛砖引玉】使用 ES6 的类继承加速 SPA 开发

发表于 2018-08-28 | 分类于 工具链开发

【抛砖引玉】使用 ES6 的类继承加速 SPA 开发

背景

随着 ES6 和 SPA 框架的普及,前端开发已经在从以往的页面为主体向组件为主体进行改变,而组件的重用可以使用类继承来加速 SPA 开发。下面就是一个简单的继承实现的例子。

初始化项目

本文使用 angular6.0 + ng-zorro1.4 作为范例,首先初始化项目 ng-extents-components:

1
2
3
ng new ng-extents-component
cd ng-extents-component
ng add ng-zorro-antd

初始化完成后执行 ng serve 即可访问 http://localhost:4200 如图:

新增 LoginPageComponent 和 RegisterPageComponent 并配置路由

ng-zorro 为我们提供了基于 Schematics 的模板,我们可以直接选择适应的模板新建 LoginPageComponent 和 RegisterPageComponent :

1
2
ng g ng-zorro-antd:form-register -p app --styleext='scss' --name=register-page
ng g ng-zorro-antd:form-normal-login -p app --styleext='less' --name=login-page

新建完成后修改 src/app/app.module.ts 的 imports 字段,添加响应式表单支持并配置路由:

1
2
3
4
5
6
7
...
ReactiveFormsModule,
RouterModule.forRoot([
{path:'login',component:LoginPageComponent},
{path:'register',component:RegisterPageComponent}
]),
...

随后修改 src/app/app.module.ts 添加 router-outlet:

1
2
3
4
5
<!-- NG-ZORRO -->
<!--<a href="https://github.com/NG-ZORRO/ng-zorro-antd" target="_blank" style="display: flex;align-items: center;justify-content: center;height: 100%;width: 100%;">-->
<!--<img height="300" src="https://img.alicdn.com/tfs/TB1NvvIwTtYBeNjy1XdXXXXyVXa-89-131.svg">-->
<!--</a>-->
<router-outlet></router-outlet>

完成后即可访问 http://localhost:4200/login 和 http://localhost:4200/register 如图所示:


使 LoginPageComponent 继承基类 TablePageBasicComponent

LoginPageComponent 和 RegisterPageComponent 实际其本质均为响应式表单,登录和注册操作的本质均为提交表单,我们完全可以为两者抽象一个基类 TablePageBasicComponent 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {Component} from '@angular/core';
import {FormBuilder, FormGroup} from "@angular/forms";
import {HttpClient} from "@angular/common/http";

@Component({
template: ''
})
export class TablePageBasicComponent {
validateForm: FormGroup; // 存放实例化后的 FormGroup
submitUrl: string = ''; // 存放 submit 方法的 url ,子类可以对其重写

constructor(public fb: FormBuilder, public http: HttpClient) { // 基类的构造函数,将所有的类依赖以 public 的方式注入,以供子类访问
}

ngOnInit(): void { // 基类 ngOnInit 方法
console.log('TablePageBasicComponent.ngOnInit()');
}

submitForm(): void { // 基类 submitForm 方法
console.log('TablePageBasicComponent.submitForm()');
console.log(this.validateForm.value);
this.http.post(this.submitUrl,this.validateForm.value).subscribe(resp => {
console.log(resp)
});
}
}

我们在子类使用时,只需要修改 submitUrl 即可对提交的 url 进行修改,但因为使用了响应式表单,我们还需在各个子类内重写 ngOnInit(): void。

下面使 LoginPageComponent 继承自 TablePageBasicComponent,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component } from '@angular/core';
import {TablePageBasicComponent} from "../basic-components/table-page.basic.component";
import {Validators} from "@angular/forms";

@Component({
selector: 'app-login-page',
templateUrl: './login-page.component.html',
styleUrls: ['./login-page.component.less']
})
export class LoginPageComponent extends TablePageBasicComponent{
submitUrl='http://yapi.youhujia.com/mock/42/demo/login';

ngOnInit(): void {
super.ngOnInit();
console.log('LoginPageComponent.ngOnInit()');
this.validateForm = this.fb.group({
userName: [ null, [ Validators.required ] ],
password: [ null, [ Validators.required ] ],
remember: [ true ]
});
}
}

点击登录按钮后,我们可以看到以下输出:

1
2
3
4
table-page.basic.component.ts:16 TablePageBasicComponent.ngOnInit()
login-page.component.ts:15 LoginPageComponent.ngOnInit()
table-page.basic.component.ts:20 TablePageBasicComponent.submitForm()
table-page.basic.component.ts:21 {userName: "18000000000", password: "test", remember: true}

同样我们可以使 RegisterPageComponent 继承自 TablePageBasicComponent ,此处不详细实现,大家可以访问 Github 查看该项目

使用继承的价值

  • 抽象同类功能,比较有代表性的就是后台项目的列表、详情等页面
    为各类页面分别抽象基类,并可使同类页面使用统一模板,重写基类配置快速实现同类页面
  • 可快速统一修改基类的同一方法
    设想现项目有20个详情页,PM 飘过来跟你说当页面保存的时候给我加一个提示框让用户选择是否会到列表页,我们此时就可以修改基类的代码统一进行修改了
  • 可通过重写属性减少重复代码
    我们的 team 内最常用的就是通过 getDataService/sendDataService 属性来控制页面的获取/提交数据,可参考我之前的文章 从零实现 SPA 框架快速同步配置生成接口(angular2 + Easy-mock)

项目组内后台项正在使用的基类及项目结构

我们项目组内大量使用了各类组件基类的继承以简化各个组件的实现,下面仅作展现供参考讨论,不进行详细的展开。

项目组内正在使用的基类和对应功能

  • BasicComponent
    公共基类,管理所有基类共有的依赖、输入参数、事件等
  • PageBasicComponent
    页面基类,继承自 BasicComponent,管理所有页面的面包屑导航、弹框处理等
  • TablePageBasicComponent
    列表页基类,继承自 PageBasicComponent,管理所有列表页的数据加载、按钮控制等
  • CreatePageBasicComponent
    新建页基类,继承自 PageBasicComponent,管理所有新建页的数据提交、按钮控制等
  • DetailPageBasicComponent
    详情页基类,继承自 CreatePageBasicComponent,管理所有详情页的状态切换、数据提交、按钮控制等
  • TableViewBasicComponent
    列表组件基类,继承自 BasicComponent,通常嵌入到 TablePageBasicComponent,提供输入参数/数据接口/各类事件来管理业务数据的展示和事件触发
  • FormViewBasicComponent
    表单组件基类,继承自 BasicComponent,通常嵌入到 CreatePageBasicComponent 或 DetailPageBasicComponent,提供输入参数/数据接口/各类事件来管理业务数据的展示和事件触发
  • SelectorViewBasicComponent
    选择器组件基类,继承自 BasicComponent,通常嵌入到 FormViewBasicComponent,提供输入参数/数据接口/各类事件来管理业务数据的展示和事件触发

对应的项目结构

  • entites
    存放所有后端接口 Service ,参考我之前的文章 从零实现 SPA 框架快速同步配置生成接口(angular2 + Easy-mock)
  • pipes
    存放所有自定义 pipes
  • services
    存放所有自定义 services
  • basics
    存放所有基类
  • layouts
    存放所有业务无关的布局组件
  • views
    存放所有业务相关的 views 组件,通常继承自 TableViewBasicComponent/FormViewBasicComponent/SelectorViewBasicComponent
  • pages
    存放所有关联后端数据的 pages 组件,通常继承自 PageBasicComponent 的各子类

结语

本文只是抛砖引玉简单的实现了一个类继承的项目,而实际开发过程中还是需要结合需求对项目进行梳理。感兴趣的同学可在我的博客进行留言我会定期进行回复。

Xampp 修改 Apache 默认端口号 80/443 使其与 Nginx 共存

发表于 2018-08-27 | 分类于 mediawiki

背景

近期在研究搭建mediawiki,因服务器的 80 端口被 nginx 占用,所有 xampp 的 apache 在启动时会提示 80 端口被占用,在此记录下修改 xampp 的 apache 端口号的过程

修改 apache 端口号 80 => 8888

打开配置文件 /opt/lampp/etc/httpd.conf,修改 Listen 的 80 => 8888

1
2
3
4
5
# vi /opt/lampp/etc/httpd.conf
...
# Listen 80
Listen 8888
...

修改 apache 端口号 443 => 8443

打开配置文件 /opt/lampp/etc/extra/httpd-ssl.conf,修改 Listen 的 80 => 8888

1
2
3
4
5
# vi /opt/lampp/etc/extra/httpd-ssl.conf
...
# Listen 443
Listen 8443
...

修改 xampp 启动脚本

因 xampp 启动脚本內对 80 和 433 端口号是否占用使用硬编码判断,所以我们要直接对其进行修改

1
2
3
4
5
6
7
8
# vi /opt/lampp/xampp
...
# if testport 80
if testport 8880
...
# if test $ssl -eq 1 && testport 443
if test $ssl -eq 1 && testport 8443
...

【转】微信小程序中使用lodash

发表于 2018-08-27 | 分类于 小程序

探究过程

不想看探究过程的, 结论在最后.

  • 安装 lodash.get, 拷贝文件node_modules/lodash.get/index.js至 utils/lodash.get/index.js, 然后直接 require('./utils/lodash.get/index.js'), 可以正常使用

  • 拷贝 node_modules/lodash/lodash.js 文件至 utils/lodash/lodash.js, 然后直接 require('./utils/lodash/lodash.js'), 出现

1
Uncaught TypeError: Cannot read property 'prototype' of undefined

mina-lodash1.png

跳转至源码 发现 Array 不存在, 因为 freeGlobal 和 freeSelf 都为 false, 因为微信直接注入了 window 和 self;

mina-lodash2.png

mina-lodash3.png

mina-lodash5.png

最终, Array = (Function('return this')()).Array 为 undefined.

所以, 只需要替换 root 的值即可, 从lodash的源码中发现, lodash 会pick以下属性
mina-lodash6.png

使用try catch 监测支持属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
try { root.Array = Array } catch (e) { console.log('Array not support in MINA, skip') }
try { root.Buffer = Buffer } catch (e) { console.log('Buffer not support in MINA, skip') }
try { root.DataView = DataView } catch (e) { console.log('DataView not support in MINA, skip') }
try { root.Date = Date } catch (e) { console.log('Date not support in MINA, skip') }
try { root.Error = Error } catch (e) { console.log('Error not support in MINA, skip') }
try { root.Float32Array = Float32Array } catch (e) { console.log('Float32Array not support in MINA, skip') }
try { root.Float64Array = Float64Array } catch (e) { console.log('Float64Array not support in MINA, skip') }
try { root.Function = Function } catch (e) { console.log('Function not support in MINA, skip') }
try { root.Int8Array = Int8Array } catch (e) { console.log('Int8Array not support in MINA, skip') }
try { root.Int16Array = Int16Array } catch (e) { console.log('Int16Array not support in MINA, skip') }
try { root.Int32Array = Int32Array } catch (e) { console.log('Int32Array not support in MINA, skip') }
try { root.Map = Map } catch (e) { console.log('Map not support in MINA, skip') }
try { root.Math = Math } catch (e) { console.log('Math not support in MINA, skip') }
try { root.Object = Object } catch (e) { console.log('Object not support in MINA, skip') }
try { root.Promise = Promise } catch (e) { console.log('Promise not support in MINA, skip') }
try { root.RegExp = RegExp } catch (e) { console.log('RegExp not support in MINA, skip') }
try { root.Set = Set } catch (e) { console.log('Set not support in MINA, skip') }
try { root.String = String } catch (e) { console.log('String not support in MINA, skip') }
try { root.Symbol = Symbol } catch (e) { console.log('Symbol not support in MINA, skip') }
try { root.TypeError = TypeError } catch (e) { console.log('TypeError not support in MINA, skip') }
try { root.Uint8Array = Uint8Array } catch (e) { console.log('Uint8Array not support in MINA, skip') }
try { root.Uint8ClampedArray = Uint8ClampedArray } catch (e) { console.log('Uint8ClampedArray not support in MINA, skip') }
try { root.Uint16Array = Uint16Array } catch (e) { console.log('Uint16Array not support in MINA, skip') }
try { root.Uint32Array = Uint32Array } catch (e) { console.log('Uint32Array not support in MINA, skip') }
try { root.WeakMap = WeakMap } catch (e) { console.log('WeakMap not support in MINA, skip') }
try { root._ = _ } catch (e) { console.log('_ not support in MINA, skip') }
try { root.clearTimeout = clearTimeout } catch (e) { console.log('clearTimeout not support in MINA, skip') }
try { root.isFinite = isFinite } catch (e) { console.log('isFinite not support in MINA, skip') }
try { root.parseInt = parseInt } catch (e) { console.log('parseInt not support in MINA, skip') }
try { root.setTimeout = setTimeout } catch (e) { console.log('setTimeout not support in MINA, skip') }

在微信Android中测试后, 发现小程序不支持以下属性
mina-lodash7.png

结论

① 直接引入 lodash modularize 之后的包可以解决

1
2
3
npm install lodash.get
let get = require('./your_copy_path/lodash.get/index');
// 直接使用 get(obj, path);

② 将lodash4.16.6 lodash/lodash.js:416 中

1
var root = freeGlobal || freeSelf || Function('return this')();

替换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var root = {
Array: Array,
Date: Date,
Error: Error,
Function: Function,
Math: Math,
Object: Object,
RegExp: RegExp,
String: String,
TypeError: TypeError,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval
};

转自 SHANG殇

NodeJS 微信公共号开发 - 实现微信网页授权获取用户信息

发表于 2018-05-08 | 分类于 NodeJS

背景

使用 NodeJS 进行微信公共号开发,我们经常需要获取当前微信用户的用户信息,本文就使用 Express + mongoose 结合 Token 实现微信网页授权获取用户信息。

微信网页授权流程

微信认证流程

上图为结合 Token 进行微信授权的过程,大致流程如下:

微信公共号 SPA 每次发起 AJAX 请求都会在 header 内附加本地缓存的用户鉴权信息(通过用户信息结合时间戳通过 JWT 生成的 Token),服务器会在所需验证的接口调用前使用中间件对 Token 进行验证,如果合法则继续执行该接口的逻辑,否则则返回 401 错误,同时生成微信网页授权的 authorize URL 供 SPA 进行跳转。

微信公共号 SPA 在接口返回 401 错误时会跳转到错误内返回的 URL(即微信网页授权的 authorize URL),用户授权成功后微信会附带微信网页授权的 Code 将页面重定向到 authorize URL 内的 redirect_uri ,而该重定向的 URL 其实为后端接口,后端在通过 Code 获取微信用户信息完成新增/查询用户的行为后,再根据用户信息和时间戳通过 JWT 生成 Token,结合 redirect_uri 内记录的 spa 发起 401 错误请求的页面附带 Token 进行重定向。

微信公共号 SPA 通过 url 内的 query 参数缓存 Token 信息,并在每次发起 AJAX 请求内在 header 内附带 Token。

其实上述过程的很多行为都是贯穿所有的接口内的,下面就对具体实现进行详细描述。

基于 Token 的用户鉴权

本文使用 JWT(JSON WEB TOKEN) 生成用户 Token, 使用基于 Token 的用户鉴权,大概要点如下:

  • SPA 在发送 AJAX 请求时在 header 内附带 Token
  • SPA 通过登录或某种全局字段获取并更新 Token
  • 服务器在需要鉴权的接口前使用中间件对 Token 鉴权
  • 服务器实现 Token 的生成和返回

服务器实现用户的 mongoose 模型定义

本文服务器使用 Express + mongoose 实现,首先我们需要在服务器实现用户的 mongoose 模型定义,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
let mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
// id 为自增主键,token 存储鉴权信息,state 为用户状态预留,其他为微信会返回的各用户信息字段
let UserSchema = new mongoose.Schema({
id: {
type: Number,
unique: true,
index: true
},
token: {
type: String,
index: true
},
openid: {
type: String,
unique: true
},
phone: {
type: String,
trim: true
},
nickname: {type: String},
sex: {type: Number},
language: {type: String},
city: {type: String},
province: {type: String},
country: {type: String},
headimgurl: {type: String},
privilege: {type: Array},
unionid: {type: String},
state:{
type:Number,
default:1
}
});

// 处理用户 id 自增
UserSchema.pre('save', function (next) {
let doc = this;
if (doc.isNew) {
User.findOne({}, 'id', {sort: {id: -1}}).then(d => {
doc.id = (d ? d.id : 0) + 1;
next();
});
} else {
next();
}
});
// 重写 toJSON 在返回用户信息前隐藏敏感字段
UserSchema.methods.toJSON = function () {
const User = this;
const UserObject = User.toObject();
delete UserObject._id;
delete UserObject.__v;
delete UserObject.openid;
delete UserObject.unionid;
return UserObject;
};

const User = mongoose.model('User', UserSchema);
module.exports = {User};

SPA 在发送 AJAX 请求时在 header 内附带 Token

服务器需要根据 Token 在部分接口内对用户进行鉴权,SPA 需要在发送 AJAX 请求的时候进行封装,在 header 内添加 Token,以下代码为 angular4 项目在 header 内的 Authenticate 字段添加 localStorage.token 的服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {Injectable} from '@angular/core';
import {Headers, RequestOptions} from '@angular/http';
@Injectable()
export class CustomRequestOptions extends RequestOptions {
constructor () {
const headers = new Headers();
headers.append('Accept', 'application/json');
headers.append('Access-Control-Allow-Origin', '*'); // 处理跨域
headers.append('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE,PATCH, OPTIONS');
headers.append('Access-Control-Allow-Headers', 'X-Requested-With');
headers.append('Content-Type', 'application/json');
headers.append('crossDomain', 'true');
headers.append('Authenticate', localStorage.token || 'empty'); // 附带 Token
super({headers: headers});
}
}

服务器的鉴权中间件实现

首先在是 User 模型内添加 authenticate 静态方法,通过 Token 查询用户信息支持鉴权:

1
2
3
4
UserSchema.statics.authenticate = async function (token) {
const User = this;
return await User.findOne({'token': token});
};

然后在需要鉴权的接口前添加中间件,根据请求 header 内的 Token 信息调用 User.authenticate 静态方法实现鉴权,成功则在 req 内添加用户信息,否则返回 401 错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const app = require('express');
const router = app.Router();

const {User} = require('../../models/user');

// token 验证
router.all('/**', async function (req, res, next) {
try {
let user = await User.authenticate(req.header('Authenticate') || 'empty');
if (!user) {
res.status(401);
res.end();
} else {
req.user = user;
req.body.userId = user.id;
next();
}
} catch (e) {
next(e)
}
});

// 此后代码才需鉴权,并可在 req.user 内拿到 Token 对应的用户信息
router.use('/user', require('./user/route'));
router.use('/child', require('./child/route'));
router.use('/resources', require('./resources/route'));
router.use('/appointment', require('./appointment/route'));

module.exports = router;

微信网页授权的实现

微信网页授权 首先需要在 微信后台 => 公共号设置 => 功能设置 配置网页授权后重定向的页面域名,并将指定 .txt 文件允许在域名下的根目录进行访问(Express 的情况下直接丢到静态资源目录下即可)
配置网页授权后重定向的页面域名
配置网页授权后重定向的页面域名
服务器则需要实现以下功能:

鉴权失败时引导用户进入授权页面

当服务器鉴权失败时,除返回 401 错误外,我们还应生成用户授权页面的 authorize URL 供 SPA 跳转。关键两个字段如下:
appid:公共号ID,在微信后台可进行查询
redirect_uri:用户授权完成后跳转到的 URL,此处我们填写一后端接口,并获取 401 错误时 SPA 所在的 URL 作为参数传递为 redirect 作准备。
我们将鉴权中间件修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.all('/**', async function (req, res, next) {
try {
let user = await User.authenticate(req.header('Authenticate') || 'empty');
if (!user) {
let oauthUrl= `http://${req.header('host')}/api/v1/weixin/oauth?url=${req.header('Referer')}`;
res.status(401).send(`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${process.env.WX_ID}&redirect_uri=${encodeURIComponent(oauthUrl)}&response_type=code&scope=snsapi_userinfo#wechat_redirect`);
res.end();
} else {
req.user = user;
req.body.userId = user.id;
next();
}
} catch (e) {
next(e)
}
});

用户授权完成后根据 Code 拉取用户信息查询/新建用户后使用 JWT 生成 Token 并重定向到鉴权失败时记录的 SPA 所在 URL

首先我们为 User 模型添加 generateAuthToken 方法,实现通过 JWT 生成 Token:

1
2
3
4
5
6
7
8
9
10
11
UserSchema.methods.generateAuthToken = async function () {
const doc = this;
doc.token = jwt.sign({
id: doc.id,
name: doc.nickname,
headimgurl: doc.headimgurl,
sex: doc.sex,
timestamp: +new Date()
}, process.env.JWT_SECRET).toString();
return await doc.save();
};

然后实现之前网页授权成功后跳转到的 URL。该页面为后端接口,在微信网页授权后微信直接通过 get 的方式进行访问,所以我们在该接口内除了可以通过 query 参数获取到用户 Code 外,还可通过之前记录的 SPA 鉴权 401 所在 URl 通过 Redirect 的方式附带 Token 返回到 SPA 之前的鉴权失败页面。具体接口实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const express = require('express');
const router = express.Router();
const axios = require('axios');

const {User} = require('../../../models/user');

router.get('/', async function (req, res, next) {
try {
// 通过微信的 App ID、Secret 和用户的 Code 获取用户的 access token
let userAccessToken = (await axios.get(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${process.env.WX_ID}&secret=${process.env.WX_SECRET}&code=${req.query.code}&grant_type=authorization_code`)).data;
if (userAccessToken.errcode) {
userAccessToken.param = 'userAccessToken';
next(new Error(JSON.stringify(userAccessToken)));
return;
}
// 通过用户的 access token 从微信拉取微信的用户信息
let userInfo = (await axios.get(`https://api.weixin.qq.com/sns/userinfo?access_token=${userAccessToken.access_token}&openid=${userAccessToken.openid}&lang=zh_CN`)).data;
if (userInfo.errcode) {
userAccessToken.param = 'userInfo';
next(new Error(JSON.stringify(userInfo)));
return;
}
// 通过用户的 openid 查询,如果不存在该用户则添加
let user = await User.findOne({
openid: userInfo.openid
});
if (!user) {
user = new User(userInfo);
await user.save();
console.log('create new wx user finished');
}
// 生成用户 token
user = await user.generateAuthToken();

// 处理重定向 url,在其中添加 query 参数 token
let url=req.query.url.split('?');
let q=[];
if(url.length!==1){
q=url[1].split('&');
}
q.push(`token=${user.token}`);
// 因微信通过 get 的方式在浏览器内访问该接口,我们可以通过 res.redirect 进行重定向
res.redirect(`${url[0]}?${q.join('&')}`);
res.end();
} catch (e) {
next(e);
}
});

module.exports = router;

SPA 添加 query 内 token 参数的缓存

我们已经实现后端通过微信网页授权的 Code 生成/查询用户并返回 Token 的全部逻辑,下面需要对所有可以被重定向的页面添加对 url 内 query 参数 token 的全局处理,进行缓存后删除 url 内 query 的 token 字段防止分享时附带 token 信息。在 SPA 应用的入口 html 文件的 header 最上方添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
function delParam(url,paramKey){
var urlParam = url.substr(url.indexOf("?")+1);
var beforeUrl = url.substr(0,url.indexOf("?"));
var nextUrl = "";

var arr = [];
if(urlParam!==""){
var urlParamArr = urlParam.split("&");

for(var i=0;i<urlParamArr.length;i++){
var paramArr = urlParamArr[i].split("=");
if(paramArr[0]!==paramKey){
arr.push(urlParamArr[i]);
}
}
}

if(arr.length>0){
nextUrl = "?"+arr.join("&");
}
url = beforeUrl+nextUrl;
return url;
}
if(getQueryString("token")){
localStorage.token = getQueryString("token");
location.href = delParam(window.location.href,"token");
}

使用微信开发者工具查看效果

至此微信网页授权获取用户信息的全部过程已经实现,从微信开发者工具查看效果如下:
微信网页授权效果
查看 Network 梳理一下访问了那些页面和接口:

  • baohejm-dev.youhujia.com/
    SPA 页面

  • baohejm-api-dev.youhujia.com/api/v1/weixin/resources/department
    SPA 页面访问的接口,返回 401 和微信网页授权的 authorize URL

  • open.weixin.qq.com/connect/oauth2/authorize?appid={appid}&redirect_uri=http%3A%2F%2Fbaohejm-api-dev.youhujia.com%2Fapi%2Fv1%2Fweixin%2Foauth%3Furl%3Dhttp%3A%2F%2Fbaohejm-dev.youhujia.com%2F&response_type=code&scope=snsapi_userinfo
    微信授权页面,注意 query 参数内的 redirect_uri ,其实值为 http://baohejm-api-dev.youhujia.com/api/v1/weixin/oauth?url=http://baohejm-dev.youhujia.com/

  • baohejm-api-dev.youhujia.com/api/v1/weixin/oauth?url=http://baohejm-dev.youhujia.com/&code={code}
    后台根据 Code 进行用户添加/查询并生成 Token 并重定向到 query 参数 url(根据 401 时记录的 SPA 的所在页面)

  • baohejm-dev.youhujia.com/?token={token}
    重新附带 token 访问 SPA 页面

使用 Express 实现一个简单的 SPA 静态资源服务器

发表于 2018-05-04 | 分类于 NodeJS

背景

限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案。

为 SPA 应用添加入口(index.html)的 sendFile

当 SPA 应用开启 html5 mode 的情况下,指定 url 下(<base href="/">的情况为/)的全部请求都会访问入口文件(一般情况下是 index.html),然后 SPA 应用会根据 url 再去决定访问的实际页面。
所以我们需要为全部路径添加 sendFile 来发送 index.html 文件内的内容,并将其缓存实际设为0,代码如下:

1
2
3
app.use("/**", function (req, res) {
res.sendfile(staticPath+"/index.html", {maxAge: 0});
});

为 SPA 应用添加其他静态资源

由于 Express 中间件的特性,在 index.html 的 sendFile 之前我们需要将其他静态资源进行处理,具体代码如下:

1
2
3
const options = process.env.env == 'prod' ? {maxAge: '3d'} : {maxAge: '1m'};
app.use(express.static(path.join(__dirname, staticPath), options));
});

SPA 静态资源服务器的全部代码

下面是 SPA 静态资源服务器 app.js 的全部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
const path = require('path');
const http = require('http');

const app = express();
const staticPath=process.env.static ||'dist';
const port=process.env.port || 3000;

const options = process.env.env == 'prod' ? {maxAge: '3d'} : {maxAge: '1m'};
app.use(express.static(path.join(__dirname, staticPath), options));

app.use("/**", function (req, res) {
res.sendfile(staticPath+"/index.html", {maxAge: 0});
});

const server = http.createServer(app);
server.listen(port);

console.log(`env:${process.env.env}`);
console.log(`path:${staticPath}`);
console.log(`port:${port}`);

执行以下命令即可指定 SPA 项目路径和端口号启动服务器了

1
export static=www&&export port=8101 && export env=prod && node ./app.js

结语

SPA 静态资源服务器的实现较为简单,其实只需要理解了 Express 的中间件和 SPA 应用在使用 html5 mode 时的解析即可。

1234
Dongsj

Dongsj

一个喜欢晒饲养员的程序猿

18 日志
8 分类
35 标签
GitHub E-Mail
© 2018 — 2020 Dongsj
Hosted by Coding Pages
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4