rollup配置、登录配置、埋点

文章介绍了如何使用Rollup构建项目,配置Babel以支持ES6+TS,使用Koa作为Node.js服务并实现跨域、JWT验证、CSRF保护和XSS防御。同时讨论了单点登录(SSO)和前端埋点数据收集的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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))
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值