rollup.config.js
const babel = require('rollup-plugin-babel');
module.exports = {
input: './src/index.js',
output: {
file: './dist/bundle.js',
format: 'umd',
// iife 立即执行函数
name: 'zwServer'
},
treeshake: false,
plugins: [
babel({
runtimeHelpers: true,
extensions: ['.js', '.ts'],
exclude: "node_modules/**",
externalHelpers: true
})
]
}
.babelrc :babel 的解析
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"loose": true, // 满足基本使用
"targets": "node 16",
"useBuiltIns": "usage",
"corejs": {
// 核心解析 pollify 的能力 async -> generator
// acorn 编译
"version": "3.32",
"proposals": true
}
}
]
],
"plugins": [
// 装饰器的能力
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
index.js
console.log('hello world!')
module.js
const random = Math.random()
pakage.json
"scripts": {
"build": "rollup -c",
"start": "rollup -c -w",
"dev": "nodemon ./dist/bundle.js"
},
nodemon ./dist/bundle.js
nodemon: node 端的一个监控
koa 服务:index.js
import Koa from koa
import Router from 'koa-router'
const app = new koa()
const router = new Router()
import { controllers } from './utils/decorator';
import bodyParser from 'koa-bodyParser'
// 请求需要抽离出来
const app = new Koa();
const router = new Router();
const PREFIX = ''
// 跨域配置:
app.use(bodyParser())
// cors 6个
app.use(async(ctx, next) => {
ctx.set('Access-Control-Allow', '*')
ctx.set('Access-Control-Allow-Headers', "Content-type, Content-Length, Authorization, Accpet, X-Requested-With");
ctx.set('Access-Control-Allow-Methods', "PUT, GET, POST, DELETE, OPTION");
ctx.set('Content-Type', "application/json;charset=utf-8");
ctx.set('Content-Type', '')
if(ctx.request.method.toLowerCase() === 'options') {
ctx.state = 200
} else {
await next()
}
})
// const apis = [
// {
// method: 'get',
// path: '/api/book',
// fn: asynx (ctx) => {
// ctx.body = {
// data: ['1', '2']
// }
// },
// {
// method: 'get',
// path: '/api/book/all',
// fn: asynx (ctx) => {
// ctx.body = {
// data: ['1', '2']
// }
// },
// }
// ]
// apis.forEach({method, path, fn}) => {
// router[method](path, fn)
// }
// router.get('api/book',async (ctx)=>{
// ctx.body = {
// data: ['1','2']
// }
// })
// router.get('api/book/all',async (ctx)=>{
// ctx.body = {
// data: ['3','4']
// }
// })
//
controllers.forEach((item) => {
let { method, path, handler, constructor } = item;
const { prefix } = constructor;
if(prefix) path = `${prefix}${path}`;
router[method](`${PREFIX}${path}`, handler)
});
app.use(router.routes())
api.listen(3008, ()=> {
console.log('server is running on 3008')
})
BFF: backend for frontend
@controller('book')
public class HelloController {
@RequestMapping('all')
public GetHelloString(HttpServleRequest request...)
// 后端给出
}
// book/all 前端请求
utils/decorator.js
export const RequestMethod = {
GET: "get",
POST: "post",
PUT: "put",
DELETE:"delete"
}
export const controllers = [];
export function Controller(prefix = '') {
return function(target) {
target.prefix = prefix;
}
}
export function RequestMapping(method = '', url = '') {
return function(target, name) {
let path = url || `/${name}`;
const item = {
path,
method,
handler: target[name],
constructor: target.constructor
}
controllers.push(item);
}
}
controllers/book.js
import { Controller, RequestMapping, RequestMethod } from "../utils/decorator";
@Controller('/book')
export default class BookController {
@RequestMapping(RequestMethod.GET, '/all')
async getAllBooks(ctx) {
ctx.body = {
data: ['一眼精通JS', "JS 从入门到放弃"]
}
}
}
// 方法, 路径,函数。
// router.get('/book/all', (ctx) => {
// ctx.body = [];
// })
跨域:浏览器限制的
// 同源策略: 协议 域名 端口号
// 前后端跨域
// dev server 不会跨域
鉴权:
- Jwt 是什么:
- JSON Web Token组成 base64 有过期时间
- Header:标明使用的是jwt
- Payload:负载
- Signature: 签名
- (Header).(Header).(Header).(Payload)再进行一个加密 base64
fbiuwybefoiaewbrAHSD3iugh2piqubp4. -> { alg: "HS256" }
q39ui428bqo8rwe3uybf8q3ybvf. -> { username: 'zhangsan' }
fbiuwybefoiaewbrAHSD3iugh2piqubp4.q39ui428bqo8rwe3uybf8q3ybvf -> HS256 根据这个算法和张三的信息算出李四的账号信息
加一小撮盐。
2o4grupi3qarfoiuwbfiob2973qbp83aygpibypgarwupeiru==
2o4grupi3qarfoiuwbfiob2973qbp83aygpibypgarwupeiru==
// ->
fbiuwybefoiaewbrAHSD3iugh2piqubp4. -> { alg: "HS256" }
q39ui428bqo2342342wf23edrtg. -> { username: 'lisi' }
2o4grupi3qarfoiuwbfiob2973qbp83dfaacscccsfsdfwfes==
代码:
// node 的模块
const crypto = require('crypto')
function sign(payload, salt) {
let header = { alg: 'HS256', typ: 'JWT' };
const tokenArr = [];
// 1. 转base64
tokenArr.push(base64UrlEncode(JSON.stringify(header)));
tokenArr.push(base64UrlEncode(JSON.stringify(payload)));
// 2. 加密 加salt
const signature = encryption(tokenArr.join('.'), salt)
return [...tokenArr, signature].join('.');
}
function base64UrlEncode(str) {
return Buffer.from(str).toString('base64');
}
function encryption(value, salt) {
return crypto
.createHmac('SHA256', salt)
.update(value)
.digest('base64');
}
// 校验
function verify(token, salt) {
var segments = token.split('.');
var [h, p, s] = segments;
const signature = encryption([h, p].join('.'), salt);
return signature === s;
}
console.log(sign( {user: 'luyi'}, 'zhaowajiaoyu' ));
console.log(verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoibHV5aSJ9.ZBuNd+yDCFQwbnq9KqEd+mRemlZ2q/ZACjsHvLlUKfM=',
"luyi"
)) // false zhaowajiaoyu: true
xss:注入恶意代码 跨站脚本攻击
csrf: 利用登录态
// 论坛:
</a></html><script>
// 恶意脚本
window.open('bocai/xxx/');
</script>
jwt.js
// 登录请求如果是白名单里的 往下走
// 不是白名单里的 去拿后端给的token 进行验证 通过的话继续 否则返回401
import jwt from 'jsonwebtoken'
const SALT = "zhaowazhenniubi"
export const jwtVerify = (whitelist) => async (ctx, next) => {
if(whitelist.includes(cctx.path)){
next(ctx)
} else {
let token
try {
token = cyx.request.headers.authorization.split('Bearer')[1]
} catch(err) {
//
}
const res = await verify(token)
if(res.status === 'success') {
next()
} else {
ctx.body = {
...res,
code: 401
}
}
}
}
// 验证token
export const verify = async(token) => {
return new Promise(resolve,reject){
if(token){
jwt.verify(token,SALT,(err, data)) => {
if(err){
if(err.name === 'TokenExpiredError'){
resolve({
status: 'failed',
error: 'token 已过期'})
} else {
resolve({
status: 'failed',
error: 'token 认证失败'})
})
}
} else {
resolve({
status: 'success',
data
})
}
}
} else {
resolve({
status: 'failed',
error: 'token 不能为空'
})
}
}
}
// index.js
app.use(jwtVeriry([
'/user/login',
'/user/register'
]))
// user.js
import { Controller, RequestMapping, RequestMethod } from "../utils/decorator";
@Controller('user')
export default classs BookController {
@RequestMapping(RequestMethod.GET, '/all')
asynx getAllBooks(ctx){
ctx.body = {
data: {
username: 'zhangsan'
}
}
}
@RequestMapping(RequestMethod.POST, '/login')
asynx loginUser(ctx){
const {body} = { ctx.request }
const userService = new UserService();
ctx.body = await userService.validate(body);
}
}
service/service.js
class UserService {
async validate({username. password}) {
if(username && password){
if(username === 'zhangsan'){
if(password === '123456'){
// 说明,口令是对的,我直接生成 token 给你
const token = signatrue({ username });
return {
code: 200,
msg: '登陆成功',
status: 'success',
data {
token: ''
}
}
} else {
return {
code: 200,
msg: '密码错误',
status: 'failed',
data: void 0
}
}
} else {
return {
code: 200,
msg: '账号不存在',
status: 'failed',
data: void 0
}
}
} else {
return {
code: 200,
status: 'failed',
msg: '账号或密码不能为空',
data: void 0
}
}
}
}
单点登录:SSO
有个认证服务器 用户第一次访问的时候 跳转到认证服务器中进行登录 认证服务器给个验证标识 去别的账号用这一个验证标识就可以登录
// react里的 路由守卫
const AuthComponent = ({ children }) => {
const isToken = getToken()
if (isToken) {
return <>{children}</>
}
else {
return <Navigate to='/login' replace></Navigate>
}
}
// 保持登录态:token存在localstorage: 进入时进行校验 过期就重新颁发
埋点:
作用:为了记录用户的行为 进行一个数据的分析
通过对收集到的数据进行分析,开发人员和产品团队可以了解用户行为模式、优化产品功能、改善用户体验、评估转化率、针对不同用户群体制定营销策略等, 达到收集用户行为数据、分析用户习惯、提供数据支持、优化产品体验、提高转化率的目的
代码
1. 确定要收集的数据
2. 埋点工具
3. 代码实现
4. 数据收集和分析
- sdk的分层设计
- 用什么方法埋
- 埋点以后怎么发请求
- 数据丢失怎么办
- 稳定性怎么保证
pnpm i loadsah --filter @za/react-master
usergross
主要功能: 在应用程序或网站中插入特定的代码,以记录用户行为、操作和事件。通过在关键位置插入埋点代码,开发人员可以捕获和跟踪用户与应用程序的交互行为。这些行为可以包括点击按钮、页面浏览、提交表单、播放视频等。埋点操作可以记录关于用户行为的重要信息,例如时间戳、行为类型、页面路径、设备信息等。
方法:eps的gif 把数据带上去 异步 不会阻塞
cicd 持续集成 持续交付 持续部署
sdk
lib/track.ts
// 埋点设计: 分批上报
import { AsyncTrackQueue } from "./async-track-queue";
interface TrackData {
seqId: number;
id: string;
timestamp: number;
}
export interface UsertrackData {
msg?: string;
}
// 我们假设,这个是我埋点的 API,我每一次调用。是不是真的立马发起请求???
// 一滚动的时候,几十个埋点要发请求,我能不能先收集一波,完了一起发???
export class BaseTrack extends AsyncTrackQueue<TrackData> {
private seq = 0;
public track(data: UsertrackData) {
// 做一个收集的工作
this.addTask({
id: `${Math.random()}`,
seqId: this.seq++,
timestamp: Date.now(),
...data,
})
}
// 有一个上报的函数,需要处理一下,异步批量的逻辑
public consumeTaskQueue(data: Array<TrackData>) {
// return new Promise((resolve) => {
// const image = new Image();
// image.src = `https://luyi.com/logs?data=${JSON.stringify(data)}`;
// image.onload = () => {
// resolve(true);
// }
// })
return new Promise((resolve) => {
setTimeout(() => {
resolve(data.map((item) => item.msg))
})
}).then(console.log)
}
}
async-track-queue.ts
import { debounce } from 'lodash';
interface RequiredData {
timestamp: number | string
}
// 第三层,如果我还没上报,queueData 还有数据,用户把浏览器关了
/**
* 我是不是还有一些 task 没有报
* 所以,我用 localStorage 去存储还没上报的内容
* 等到用户再次打开的时候,我再追加上报。
*/
class TaskQueueStorableHelper<T extends RequiredData = any> {
private static instance: TaskQueueStorableHelper | null = null;
// 单例
public static getInstance<T extends RequiredData = any> () {
if(!this.instance) {
this.instance = new TaskQueueStorableHelper<T>();
}
return this.instance
}
private STORAGE_KEY = "luyi_local";
protected store: any = null;
// 我们再次打开浏览器的时候,是 constructor 执行的时候。
// 那么如果我 STORAGE_KEY 还有内容,就说明还么上报完。
constructor() {
const localStorageVal = localStorage.getItem(this.STORAGE_KEY);
if(localStorageVal) {
// 说明还没上报完,我把他放到一个地方,等到下次一起上报
try {
this.store = JSON.parse(localStorageVal)
} catch(err: any) {
throw new Error(err);
}
}
}
get queueData() {
return this.store?.queueData || [];
}
set queueData(queueData: Array<T>) {
this.store = {
...this.store,
queueData: queueData.sort((a, b) => Number(a.timestamp) - Number(b.timestamp))
};
// queueData 变化的时候,要同步一下 localStorage .
// 本质的原因,我需要更长的生命周期。。。
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.store));
}
}
// 第二层:我们要做一个收集的工作,收集多少?怎么发?
export abstract class AsyncTrackQueue<T extends RequiredData> {
// 本地存储服务
private get storableService() {
return TaskQueueStorableHelper.getInstance()
}
private get queueData(): Array<T> {
return this.storableService.queueData;
}
private set queueData(value: Array<T>) {
// 我去 set 的时候,是不是就是增加埋点数据
this.storableService.queueData = value;
if(value.length) {
this.debounceRun();
}
}
public addTask(data: T | Array<T>) {
this.queueData = this.queueData.concat(data);
}
protected abstract consumeTaskQueue(data: Array<T>): Promise<any>;
// 假如说,我想要一段时间内,没有 addTask 也就是不添加数据的时候,我再去上报。
protected debounceRun = debounce(this.run.bind(this), 500);
private run() {
const currentDataList = this.queueData;
if(currentDataList.length) {
this.queueData = [];
this.consumeTaskQueue(currentDataList);
}
}
}
apis.js
import { BaseTrack, UsertrackData } from "./track"
export class Performance {
//
public static readonly timing = window.performance && window.performance.timing;
public static init() {
if(!this.timing) {
console.warn('当前浏览器不支持 preformance API')
}
window.addEventListener('load', () => {
new BaseTrack().track(this.getTimings())
})
}
public static getTimings(): { [key in string ]: number } {
return {
dns: Performance.timing.connectEnd - Performance.timing.connectStart,
tcp: Performance.timing.domainLookupEnd - Performance.timing.domainLookupStart
}
}
}
const t = new BaseTrack();
export const sendLog = <T>(data: T) => {
t.track(data as (T & UsertrackData))
}