大家好,欢迎来到IT知识分享网。
前提铺垫:
说到微前端,也许大家想到很多,比如single-spa, qiankun等, 而这种属于对巨石项目, 做大力度的拆分, 而假如是小业务, 比如一个板块, 一个页面, 一个功能, 甚至再小到一个组件模块, 此时大家想到的也许是webpack 5的模块联邦或者借助Eluxjs。
但是这些都不算灵活,比如大部分的业务场景中,webpack就不适合升级, 而且
用微模块,常想到的是借助于打包工具:
- 静态编译:微模块作为一个NPM包被安装到工程中,通过打包工具(如webpack)正常编译打包即可。这种方式的优点是代码产物得到打包工具的各种去重和优化;缺点是当某个模块更新时,需要整体重新打包。
- 动态注入:利用
ModuleFederation
,将微模块作为子应用独立部署,与时下流行的微前端类似。这种方式的优点是某子应用中的微模块更新时,依赖该微模块的其它应用无需重新编译,刷新浏览器即可动态获取最新模块;缺点是没有打包工具的整体编译与优化,代码和资源容易重复加载或冲突。
其实还有一点就是使用静态资源的动态加载,比如hel-micro: 他实现了模块联邦sdk化,总结下,他的主要功能是远程模块方式导入
优点:
- 跨项目共享模块简单
- 免构建、热更新(注意: 这里的免构建是, 使用方免除构建)
缺点:
- 需要配套的基础设施,也就是包管理来实现灰度管理和发布
- 远程资源获取的快慢要自己来处理, 比如你已经有了cdn,那么这个问题就方便解决了
正题:
接下来就是针对缺点一, 来搭建基础设施,设计出一整套可投入生产的基建方案:
首先:我给出使用案例:视频中对于已经存在的项目A, 内嵌了模块B, 模块B是个单独的项目,当我选择发布模块B,项目A的模块B会自动更新,也可以在后台中,手动选择A项目中应该展示哪个版本的模块B:
而支持上面的灰度平台又需要我们在工程化脚本中给出支持
视频案例:(ps:视频中服务器已做迁移,暂停了)
微模块上传案例
上传的服务(golang)
功能点总结来说 前端资源文件的整理上传+版本信息获取
表设计:
分别管理模块和版本
模块:
案例数据
版本:
案例数据
这张表中的modulesInfo字段很重要, 这个是最后来实现灰度发布的关键: (而对于各公司来说,最需要处理的就是沟通,允许在用户表中,添加这个字段, 如果没有这个字段,微模块的发布不受影响,但是对于微模块来说只能够全量更新,无法实现灰度更新)
部分源码分析:
(ps:仓库放在最后了)
2: uploadModule: 脚本使用的,用来上传前端打包好的文件
原因相比较cepher较为方便,生态圈较友好,支持集群
uploadModule
上传的核心代码
func (g *GrayscaleModuleServive) UploadFiles(ctx context.Context, formData dataModels.FormData) (msg string, err error) { // 解压文件拿到本地 tmpFile, err := ioutil.TempFile("", "uploadAssets-*.tar.gz") if err != nil { return err.Error(), err } defer os.Remove(tmpFile.Name()) zipFileByteArray, err := util.GetFileBytes(formData.Assets) if err != nil { return err.Error(), err } _, err = tmpFile.Write(zipFileByteArray) if err != nil { return err.Error(), err } tmpFile.Close() // 临时解压目录 tmpDir, err := ioutil.TempDir("./", "uploadAssetsDir-*") if err != nil { return err.Error(), err } // 暂时先保留 defer os.RemoveAll(tmpDir) // 拿到解压前的文件 tarFile, err := os.Open(tmpFile.Name()) if err != nil { return err.Error(), err } defer tarFile.Close() // 解压 gzReader, err := gzip.NewReader(tarFile) if err != nil { return err.Error(), err } defer gzReader.Close() // 读取解压之后的文件流 tarReader := tar.NewReader(gzReader) for { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return err.Error(), err } // 遍历如果是目录,新建目录 path := filepath.Join(tmpDir, header.Name) if header.FileInfo().IsDir() { if err := os.MkdirAll(path, os.ModePerm); err != nil { return err.Error(), err } continue } file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(header.Mode)) if err != nil { return err.Error(), err } defer file.Close() if _, err := io.Copy(file, tarReader); err != nil { return err.Error(), err } } // 读取 hel-meta.json 文件 获取: bucketname, minioPrefix helMetaPath := filepath.Join(tmpDir, "hel-meta.json") content, err := ioutil.ReadFile(helMetaPath) if err != nil { return err.Error(), err } // 解析JSON对象 var metaDataJson dataModels.MetaData err = json.Unmarshal(content, &metaDataJson) if err != nil { return err.Error(), err } // 上传之前需要先判断数据库是否有了该版本信息(有了就默认之前文件系统中已经上传了文件) moduleInfo, err := g.grayscaleRepositories.GetModuleInfoByModuleName(metaDataJson.App.Name) if err != nil { return err.Error(), err } if moduleInfo.Id != 0 { // 存在模块 versionInfo, err := g.grayscaleRepositories.GetVersionByVersonNameAndPid(moduleInfo.Id, metaDataJson.App.Version) if err != nil { return err.Error(), err } if versionInfo.Id != 0 { // 已经存在了相同的版本,不能再上传了 return "已经有了相同的版本,请做版本升级", nil } } // 1:先更新数据库,因为数据库上传有更多的判断 msg, err = g.AddModuleInfo(ctx, dataModels.ModuleVersionReq{ModuleName: metaDataJson.App.Name, IsUseValid: formData.IsUseValid, Version: metaDataJson.App.Version, IsStable: 1}) if err != nil { return msg, err } // 2: 再更新静态文件服务 err = g.minioClient.UploadFolder(ctx, metaDataJson.App.Name, tmpDir, metaDataJson.App.Version) if err != nil { fmt.Println("UploadFolder--------------------", err) return err.Error(), err } // 3: 也可以针对性的对本地数据做处理,再做重新上传 return "成功上传", nil }
上面就是上传minio+ 更新上传信息的主要内容
其中要多判断一下的就是,相同版本无法重复上传
多提一嘴的是: minio在配置过程中,要支持相关的配置(这个百度即可),主要支持的是能够根据uri访问到文件数据即可,如果你可以直接访问到,那么忽略这个提醒
上面的上传是把文件流输出到服务端, 然后通过walk path ,把本地文件流对象存储到minio中
接下来就是详细的上传文件代码
func (m *Minio) UploadFolder(ctx context.Context, bucketName, folderPath, minioPrefix string) (err error) { // Check if bucket exists and create if it doesn't exist found, err := m.Client.BucketExists(ctx, bucketName) if err != nil { return err } if !found { // 设置存储桶的访问控制列表 ACL policy := fmt.Sprintf(`{ "Version":"2012-10-17", "Statement":[ { "Action":["s3:GetObject"], "Effect":"Allow", "Principal":{"AWS":["*"]}, "Resource":["arn:aws:s3:::%s/*"], "Sid":"", "Condition": { "StringLike": { "aws:Referer": "*" } } } ] }`, bucketName) err = m.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) if err != nil { return err } err = m.Client.SetBucketPolicy(ctx, bucketName, policy) if err != nil { fmt.Println(err) return } } strArr := strings.Split(filepath.ToSlash(folderPath), "/") fileName := strArr[1] fmt.Println("fileName", fileName) // Walk through local folder and upload all files err = filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { if err != nil { return fmt.Errorf("failed to walk through local folder: %v", err) } // Ignore directories if info.IsDir() { return nil } // Prepare object name in MinIO objectName := filepath.ToSlash(strings.Replace(path, fileName, minioPrefix, 1)) var contentType string // 获取文件扩展名 ext := filepath.Ext(path) fmt.Println("walk: path:", ext) if ext == "" { // 如果扩展名为空,使用默认的MIME类型 contentType = "application/octet-stream" } else { // 根据扩展名获取MIME类型 contentType := mime.TypeByExtension(ext) if contentType == "" { // 如果无法获取MIME类型,使用默认的MIME类型 contentType = "application/octet-stream" } } fmt.Println("walk: extesion:", contentType) fmt.Println("work objectName", objectName) // Upload object to MinIO _, err = m.Client.FPutObject(ctx, bucketName, objectName, path, minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { return fmt.Errorf("failed to upload object %s to MinIO: %v", objectName, err) } return nil }) return nil }
getRemoteConfigure
然后是拉取最新的元数据
// 调用grpc获取获取账号下面的模块版本 func (g *GrayscaleModuleServive) GetRemoteConfigure(ctx context.Context, moduleName string, userId int64, isServer bool) (res interface{}, err error) { //moduleLinkError := "xxxxx" // 容错,如果数据库异常,以下逻辑走不通, 返回链接重定向到错误界面 //moduleLink = moduleLinkError target := fmt.Sprintf("consul://%s/%s?wait=14s", g.consulAddress, "authority-service") conn, err := grpc.Dial( //consul网络必须是通的 user_srv表示服务 wait:超时 tag是consul的tag 可以不填 target, grpc.WithTransportCredentials(insecure.NewCredentials()), //轮询法 必须这样写 grpc在向consul发起请求时会遵循轮询法 grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`), ) if err != nil { return "", err } defer conn.Close() userSrvClient := pb.NewUserClient(conn) rsp, err := userSrvClient.GetUserInfoByUserId(ctx, &pb.GetUserInfoByUserIdRequest{ Id: userId, }) fmt.Println("gprc返回的数据", rsp) if err != nil { return "", err } // 根据moduleName 找到匹配的数据模块数据 moduleInfo, err := g.grayscaleRepositories.GetModuleInfoByModuleName(moduleName) if err != nil { return "", err } versionsInfo, err := g.grayscaleRepositories.GetAllversionUnderModule(moduleInfo.Id) if err != nil { return "", err } modulesInfoByUser := rsp.ModulesInfo if len(modulesInfoByUser) != 0 { fmt.Println("用户存在权限") // 当前用户存在权限 moduleIdAndVersionIdList := strings.Split(modulesInfoByUser, "|") moduleIdAndVersionIdListWithList := make([][]string, len(moduleIdAndVersionIdList)) for k, v := range moduleIdAndVersionIdList { moduleIdAndVersionIdListWithList[k] = strings.Split(v, "-") } // 找到指定的版本号 var specifyVersionId string for _, v := range moduleIdAndVersionIdListWithList { // 说明用户端对于用户的版本信息录入有问题 if len(v) != 2 { return } if v[0] == strconv.FormatInt(moduleInfo.Id, 10) { specifyVersionId = v[1] } } if len(specifyVersionId) != 0 { for _, v := range versionsInfo { if strconv.FormatInt(v.Id, 10) == specifyVersionId { // 返回指定的版本 res, err = util.GetFromUnpkgOrFileServer(moduleName, v.Version, isServer) if err != nil { return nil, err } return res, nil } } } else { // 未找到用户端指定的权限版本,此时就当作用户没有权限 goto NoAuthexEcute } } else { goto NoAuthexEcute } NoAuthexEcute: { fmt.Println("用户不存在权限或者没有找到权限") var stableVersion string var latestVersion string for _, v := range versionsInfo { if v.IsStable == 2 { stableVersion = v.Version } if v.Id == moduleInfo.LatestVersionId { latestVersion = v.Version } } // 当前用户不存在权限, 默认采用: 1: 模块设置了使用稳定版本,使用稳定版本, 2:未设置使用稳定版本,使用最新版本 if moduleInfo.IsUseValid == 2 { fmt.Println("无权限,使用了稳定版本", stableVersion) // 使用了稳定版本 if len(stableVersion) != 0 { // 如果找到了存在稳定版本 res, err = util.GetFromUnpkgOrFileServer(moduleName, stableVersion, isServer) if err != nil { return nil, err } return res, nil } else { // 虽然想使用稳定版本, 但是如果不存在稳定版本 res, err = util.GetFromUnpkgOrFileServer(moduleName, latestVersion, isServer) if err != nil { return nil, err } return res, nil } } else { fmt.Println("无权限,使用了最新版本") // 使用最新版本 if moduleInfo.LatestVersionId != 0 { res, err = util.GetFromUnpkgOrFileServer(moduleName, latestVersion, isServer) if err != nil { return nil, err } return res, nil } } } fmt.Print("一定执行这个打印") return }
注意, 用户服务(登录,注册,获取用户信息等), 作为微服务我托管到了consul,当然这里你可以根据实际的情况做出改变, 最终目的是能够获取到该用户的微模块版本权限列表,也就是上面提到的 用户表中的modulesInfo字段,然后判断用户最终能够获取的版本是什么,具体流程不清楚的, 在文章的最后我会贴出流程图
最后明确完用户可以获取到的微模块的版本信息后,就是根据参数,来判断是从unpkg上获取,还是说从minio中获取
func GetFromUnpkgOrFileServer(moduleName string, version string, isServer bool) (res interface{}, err error) { // unpkg: http://xxx.xx.xxx.xxx:18999/pc-com-test3@1.0.1/hel_dist/hel-meta.json // file server: http://xxx.xx.xxx.xx:9000/pc-com-test3/1.0.0/hel-meta.json var uri string if isServer { // 静态文件服务 uri = fmt.Sprintf("http://xxx.xx.xxx.xx:9000/%s/%s/hel-meta.json", moduleName, version) } else { // unpkg私服 uri = fmt.Sprintf("http://xxx.xx.xxx.xxx:18999/%s@%s/hel_dist/hel-meta.json", moduleName, version) } res, err = GetJSON(uri) return } func GetFileBytes(file multipart.File) ([]byte, error) { defer file.Close() return ioutil.ReadAll(file) } func GetContentType(filename string) string { return mime.TypeByExtension(filepath.Ext(filename)) }
其中为了优化获取元数据的加载速度,我通过redis做了数据缓存(做了之后,可以极大提到加载速度)
func GetJSON(url string) (interface{}, error) { if RedisClient == nil { fmt.Println("只初始化redis一次") InitRedis() } // 从redis读取数据 cachedData, err := RedisClient.Get(context.Background(), url).Result() if err == nil { fmt.Println("读取了缓存数据") // Data found in cache, unmarshal and return it var data interface{} if err := json.Unmarshal([]byte(cachedData), &data); err != nil { return nil, err } return data, nil } else { fmt.Println("读取redis数据失败", err) } // 读取minio数据 resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() var data interface{} err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { return nil, err } // 缓存数据到redis中 jsonData, err := json.Marshal(data) if err != nil { return nil, err } if err := RedisClient.Set(context.Background(), url, jsonData, 0).Err(); err != nil { return nil, err } return data, nil }
上传的脚本(nodejs)
上传的脚本总结来说就是读取项目的package.json, 知道要上传的微模块名称和版本号, 然后把打包好的微模块资源压缩一下, 最后交给上文提到的服务器
这里的上传脚本我是放在了脚手架中加载
脚手架的文章是(附带源码):微模块(热重载微模块的基建,附源码)
其中的核心脚本代码是这个,因为比较简单就不多做解释了
pack管控平台设计
平台具体设计方案:
模块管理界面:
二级页面:
针对模板,调用所有用户列表: 然后针对用户, 指定他能访问的版本吗,如果未指定: 那么就使用稳定版本,如果稳定版本也未指定,那么就使用最新版本
具体的简约界面设计图:
最后: 逻辑流程图
server 仓库: https://github.com/Colorssk/share-gray-services/tree/main
感谢阅读~~
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/137941.html