解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发
先来看最后的效果

问题
-
用过国际化
i18n的朋友都知道,天下苦国际化久矣,尤其是中文为母语的开发者,在面对代码中一堆的$t('abc.def')这种一点也不直观毫无可读性的代码,根本不知道自己写了啥

(如上图,你看得出来这是些啥吗) -
第二个问题就是i18n各种语言版本的语言包难以维护,随着项目变大这个语言包会越来越难以维护,能不能自动去维护呢
解决思路
所以我们前端组的小伙伴就想了个办法,能不能直接$t('中文')呢,就像下图:

这样是不是就方便看了,但是问题依然有,使用中文做key可能会在打包时乱码或者在浏览器查看下乱码,总归就是直接使用中文不安全。
因此我们想出了一个万全之策:
- 针对以前做了国际化的项目,写node扫描一遍src目录,找出所有
$t('xxxx')替换成$t('对应中文') - 由于需要改的项目是vue2编写,所以写webpack插件做以下事:
- 在打包开始前扫描src目录,找到
$t('对应中文') - 使用crc32将中文转为加密后的key,然后将
'key': '对应中文'自动追加到语言包文件中,对应的语言包会长这样:

- 在打包结束后,扫描打包后的文件,将
$t('对应中文')修改为$t(key),打包后的$t会长这样:

- 在打包开始前扫描src目录,找到
这样就不担心乱码问题了,而且可以自动维护语言包
将源码中的英文键替换成中文键
这一步之前没有写国际化的项目不用执行。
这一步只需要执行一次即可,因此不写进webpack插件中去,直接写nodejs脚本,具体步骤如下:
- 扫描src下所有文件夹,这个步骤需要用到递归,如果是文件夹就继续往下扫描,用正则表达式找出
$t('xxxx')和i18n.t('xxxx')这样的字符串 - 从之前的中文语言包中找出对应的中文并替换进源码
// replaceLang.js
const path = require('path')
const fs = require("fs");let zhLang = require("./src/utils/languages/zh.js"); // 扫描文件的根路径
let gFilePath = resolve('/src')
// 需要扫描的文件
let gExtension = ['.js','.vue','.ts','.tsx','.jsx']function resolve(dir){return path.join(__dirname,dir)//path.join(__dirname)设置绝对路径
}// 提取多级嵌套结构中的中文
function getValueByAttrs(attrs){let str = '',langObj = zhLang;attrs.forEach(item=>{str = langObj[item]if(typeof str == 'object'){langObj = langObj[item]}})return str
}/*** 替换文件夹下的英文键* * folderPath: 需要扫描替换的文件夹* extension: 需要替换的文件后缀集合*/
function replaceLangs(folderPath, extension){// 读取文件夹下文件const files = fs.readdirSync(folderPath,'utf8')files.forEach((fileName) => {const filePath = path.join(folderPath, fileName)const stats = fs.statSync(filePath)if(stats.isDirectory()) {// 如果该目录是文件夹,继续往下扫描this.replaceLangs(filePath,extension)}else if(stats.isFile()) {// 如果该目录是文件,进一步判断文件类型if(extension.includes(path.extname(fileName).toLowerCase())) {//读取文件内容const fileContent = fs.readFileSync(filePath, 'utf8')// 用正则表达式找出 `$t('xxxx')` 和 `i18n.t('xxxx')`这样的字符串let results = fileContent.match(/\$t\((.+?)\)/g)||[]let results2 = fileContent.match(/i18n.t\((.+?)\)/g)||[]results.concat(results2).forEach(info=>{let regex = /(?<=\()(.+?)(?=\))/g; let attr = info.match(regex)[0]try{let attrs = eval(attr).split('.')||[];// 从之前的语言包中获取对应的中文let str = getValueByAttrs(attrs)if(str){if(info.includes('i18n.t')){fileContent = fileContent.replace(info,"i18n.t('"+str+"')")}else{fileContent = fileContent.replace(info,"$t('"+str+"')")}}}catch(e){console.log(e)}})// 更新文件fs.writeFileSync(filePath, fileContent)}}})
}replaceLangs(gFilePath, gExtension)
webpack插件开发基础知识
可以参考插件开发文档
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。
插件可以做些什么
可以在关键时间点执行一些逻辑,要改变输出,取决于我们可以获取到什么,以及对它做些什么修改操作,比如我们可以去除注释,去除空格,合并代码,压缩文件,提取公共代码,改变配置,修改,改变输出等。
webpack插件组成
webpack插件由一下组成:
- 一个
JavaScript命名函数或JavaScript类。 - 在插件函数的prototype上定义一个
apply方法。 - 指定一个绑定到webpack自身的事件钩子。
- 处理webpack内部实例的特定数据。
- 功能完成后调用webpack提供的回调。
插件基本架构
插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个插件结构如下:
class HelloWorldPlugin {apply(compiler) {compiler.hooks.done.tap('Hello World Plugin',(stats /* 绑定 done 钩子后,stats 会作为参数传入。 */) => {console.log('Hello World!');});}
}module.exports = HelloWorldPlugin;
然后,要安装这个插件,只需要在你的 webpack 配置的 plugin 数组中添加一个实例:
// webpack.config.js
var HelloWorldPlugin = require('hello-world');module.exports = {// ... 这里是其他配置 ...plugins: [new HelloWorldPlugin({ options: true })],
};
Compiler
Compiler 负责编译,贯穿webpack的整个生命周期,Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。
常用钩子:
- beforeRun:
在开始执行一次构建之前调用,compiler.run 方法开始执行后立刻进行调用。 - watchRun:
在监听模式下,一个新的 compilation 触发之后,但在 compilation 实际开始之前执行。 - compilation:
compilation 创建之后执行。 - emit:
输出 asset 到 output 目录之前执行。 - done:
在 compilation 完成时执行。这个钩子 不会 被复制到子编译器。
Compilation
Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。
简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。
Compiler 和 Compilation 的区别
Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,compilation就会被重新创建。
注意
有些插件钩子是异步的。我们可以像同步方式一样用 tap 方法来绑定,也可以用 tapAsync 或 tapPromise 这两个异步方法来绑定。
- 当我们用
tapAsync方法来绑定插件时,必须调用函数的最后一个参数callback指定的回调函数。 - 当我们用
tapPromise方法来绑定插件时,必须返回一个pormise,异步任务完成后resolve。
Language插件开发
流程
- 在编译开始前扫描src目录下的所有文件,将
$t('中文')字符串找到,将其通过crc32加密得到key,并追加到语言包中 - 检测到文件变化时,重新执行步骤1,更新语言包
- 编译完成后,输出到dist目录前,将打包好的文件中的
$t('中文')换成$t('key'),再输出到目标目录
源码
//languagePlugin.js
const path = require('path')
const { crc32 } = require('crc')
const fs = require("fs");// 扫描文件的根路径
let gFilePath = resolve('/src')
// 需要扫描的文件
let gExtension = ['.js','.vue','.ts','.tsx','.jsx']function resolve(dir){return path.join(__dirname,dir)//path.join(__dirname)设置绝对路径
}class LanguagePlugin {constructor(config) {this.config = {// 指定中文语言包zh: resolve(config.zh),// 需要生成的语言包,注意需要包含中文语言包langs: config.langs.map(path => resolve(path))}// 中文语言包内容this.zh = {}// 所有语言包内容this.keyFileList = []// key引用计数,引用为0的key会被删除this.keyUseNumber = {}}apply(compiler) {// 编译开始前执行的钩子compiler.hooks.run.tap('LanguagePluginRun',() => {this.saveZhToCrc32JSON()})// 文件发生改变时执行的钩子compiler.hooks.watchRun.tap('LanguagePluginWatch',() => {this.saveZhToCrc32JSON()})compiler.hooks.emit.tapAsync('LanguagePlugin', (compilation, callback) => {const now = Date.now()const zh = this.zh// 检索每个(构建输出的)chunk:compilation.chunks.forEach(chunk => {// 检索由 chunk 生成的每个资源(asset)文件名:chunk.files.forEach(filename => {// 文件类型是js才做检测和替换var fileType = filename.split('.').pop()if(fileType==='js' && compilation.assets[filename] && compilation.assets[filename].source) {// 获取到源码var source = compilation.assets[filename].source();var newVal = sourcevar reg = /((i18n\.t)|(\$t))\((\\)*(\'|\")(.+?)(\\)*(\'|\")\)/g// 替换源码newVal = newVal.replace(reg, function(val) {let str = val.replace(/((i18n\.t)|(\$t))\((\\)*(\'|\")/g,'').replace(/(\\)*(\'|\")\)/g,'').replace(/\"/g,'\"').replace(/\'/g,'\'')let hashKey = crc32(str).toString(16)if(zh[hashKey]) {let ret = val.replace(str, hashKey)return ret}else{return val}})// 覆盖文件compilation.assets[filename] = {source: function () {return newVal},size: function () {return newVal.length}}}});});// 计时console.log((Date.now() - now) / 1000)callback();});}saveZhToCrc32JSON() {this.keyFileList = []this.keyUseNumber = {}// 判断几个XXkey.js文件存不存在,如果不存在就创建一个this.config.langs.forEach(filePath => {const { dir, base } = path.parse(filePath);try {fs.accessSync(dir, fs.constants.F_OK)try {fs.accessSync(filePath, fs.constants.F_OK)} catch(err) {console.log(filePath + '不存在,将为您自动创建')fs.writeFileSync(filePath,"const lang = {\n}\nexport default lang")}} catch (err) {console.log(dir + '不存在,将为您自动创建')fs.mkdirSync(dir)fs.writeFileSync(filePath,"const lang = {\n}\nexport default lang")}})// 提取出langs文件夹下文件的内容this.config.langs.forEach(filePath => {let langFileContent = fs.readFileSync(filePath,'utf8')// 提取{}之间的有效内容langFileContent = langFileContent.match(/\{[\S\s]+\}/g)[0]const obj = JSON.parse(langFileContent)const origin = JSON.parse(langFileContent)for(let key in obj) {// 去掉首尾空格obj[key] = obj[key].replace(/^\s*/g,'').replace(/\s*$/g,'')}this.keyFileList.push({path: filePath,val: obj,origin: JSON.stringify(origin)})for(let key in obj) {this.keyUseNumber[key] = 0}})// 更新langs文件this.updateLangsByFiles(gFilePath, gExtension)this.keyFileList.forEach((keyFileItem) => {// 删除没有使用到的keyfor(let key in this.keyUseNumber) {if(this.keyUseNumber[key] === 0) {console.log("["+keyFileItem.val[key]+"]没有被使用,将为您自动删除")delete keyFileItem.val[key]}}// 语言包有改动才更新if(JSON.stringify(keyFileItem.val) !== keyFileItem.origin) {console.log("更改了文件"+keyFileItem.path)let content = 'const lang = {'for(let key in keyFileItem.val) {content += '\n"' + key + '":"' + keyFileItem.val[key] + '",'}// 去掉最后一个逗号content = content.substring(0, content.length - 1)content += '\n}\nexport default lang'fs.writeFileSync(keyFileItem.path, content)}if(this.config.zh === keyFileItem.path) {this.zh = keyFileItem.val}})}/*扫描文件更新语言包*/updateLangsByFiles(folderPath, extension) {// 读取文件夹下的内容const files = fs.readdirSync(folderPath,'utf8')files.forEach((fileName) => {const filePath = path.join(folderPath, fileName)const stats = fs.statSync(filePath)// 判断是文件夹还是文件if(stats.isDirectory()) {this.updateLangsByFiles(filePath,extension)}else if(stats.isFile()) {if(extension.includes(path.extname(fileName).toLowerCase())) {const fileContent = fs.readFileSync(filePath, 'utf8')let results = fileContent.match(/((i18n\.t)|(\$t))\((\'|\")(.+?)(\'|\")\)/g)if(results) {results.forEach(result => {// 获取中文并且获取crclet str = result.replace(/((i18n\.t)|(\$t))\((\'|\")/g,'').replace(/(\'|\")\)/g,'').replace(/\"/g,'\\"').replace(/\'/g,"\\'")let hashKey = crc32(str).toString(16)// 更新语言包数据以及计数数据this.keyFileList.forEach((keyFileItem) => {const obj = keyFileItem.valif(!obj[hashKey]) {obj[hashKey] = strthis.keyUseNumber[hashKey] = 0}this.keyUseNumber[hashKey]++})})}}}})}
}module.exports = LanguagePlugin;
插件使用
- 项目的根目录下添加
languagePlugin.js - package.json 的 devDependencies 加上 “crc”:“^4.3.2”,运行npm install安装crc依赖
- vue.config.js中使用该插件
configureWebpack(config) {return {plugins: [new LanguagePlugin({zh: '/src/langs/zhKey.js',langs: [// 这里有中文版,中文繁体版,英文版语音包'/src/langs/zhKey.js','/src/langs/zhTWKey.js','/src/langs/enKey.js']})]}
}
- 修改国际化i18n插件引入语言包的路径
- 运行npm run dev,能看到语言包自动更新,页面效果正常。
- 运行npm run build正常打包。
后记
这个插件其实不难,就是我的正则表达式水平不太行,后面我可能会专门花时间去学习正则表达式。
webpack的各种钩子我理解的不是很深刻,目前这个代码里的钩子都是我一个一个试出来的。
关于这个插件我其实还有一些想法想实现,比如可以调用AI翻译的API自动翻译出来对应的语言包,国际化从此以后完全傻瓜式啦。
还可以写个vite版本的插件,不过这是以后的事情啦。。。公司的国际化改造告一段落,撒花~
相关文章:
解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发
先来看最后的效果 问题 用过国际化i18n的朋友都知道,天下苦国际化久矣,尤其是中文为母语的开发者,在面对代码中一堆的$t(abc.def)这种一点也不直观毫无可读性的代码,根本不知道自己写了啥 (如上图,你看得出…...
【Django】执行查询—F()表达式
F() F()可以实现将模型字段值与同一模型中的另一字段做比较。举个例子看一下: class Entry(models.Model):...number_of_comments models.IntegerField(default0)number_of_pingbacks models.IntegerField(default0)...找到所有 number_of_pingbacks 大于 numbe…...
202112CSPT4磁盘文件操作
题意:有n个id号,m段空间,k个操作: 0 0 0:从L开始到R或遇到第一个其他非空id号为止,写入 i d id id号以及值 v a l val val;如果成功写入则输出写入成功的最右位置,否则输出-1 1 1 1:若 [ L , …...
5GC SBA架构
协议标准:Directory Listing /ftp/Specs/archive/23_series/23.501/ (3gpp.org) NF描述说明NSSFNetwork Slice Selection Function网络切片选择,根据UE的切片选择辅助信息、签约信息等确定UE允许接入的网络切片实例。NEF Network Exposure Function网络开…...
《求生之路2》服务器如何选择合适的内存和CPU核心数,以避免丢包和延迟高?
根据求生之路2服务器的实际案例分析选择合适的内存和CPU核心数以避免丢包和延迟高的问题,首先需要考虑游戏的类型和对服务器配置的具体要求。《求生之路2》作为一款多人在线射击游戏,其服务器和网络优化对于玩家体验至关重要。 首先,考虑到游…...
精读服务器默认rsyslog的配置文件
rsyslog的配置文件 rsyslog.conf #### MODULES ####$ModLoad imuxsock # provides support for local system logging (e.g. via logger command) $ModLoad imjournal # provides access to the systemd journal #$ModLoad imklog # reads kernel messages (the same are read…...
Vue2:用node+express部署Vue项目
一、编译项目 命令 npm run build执行命令后,我们会在项目文件夹中看到如下生成的文件 二、部署Vue项目 接上一篇,nodeexpress编写轻量级服务 1、在demo中创建static文件夹 2、将dist目录中的文件放入static中 3、修改server.js文件 关键配置&…...
前端开发人员如何做好SEO
前端开发人员如何做好SEO SEO工作不仅限于专业人员。前端开发者也可以在日常开发中实施一些代码层面的SEO优化。 以下是一些前端常用的SEO方法: 设置合理的title、keywords、description title、keywords、description对SEO至关重要,需贴合页面内容编…...
推荐收藏!分享 PyTorch 中一些高级的索引和选择操作技巧
关于 Pytorch ,我之前分享过很多篇,喜欢的可以收藏、关注、点赞。 这一次,我准备了 20节 PyTorch 中文课程小白学 PyTorch 系列:54个超强 pytorch 操作9个技巧让你的 PyTorch 模型训练飞快!Keras 3.0发布:…...
Apache Calcite 快速入门指南
Apache Calcite 快速入门指南 参考地址:Apache Calcite 快速入门指南 - 知乎 Apache Calcite 是一个动态数据管理框架,提供了:SQL 解析、SQL 校验、SQL 查询优化、SQL 生成以及数据连接查询等典型数据库管理功能。Calcite 的目标是 One Size …...
基于MUSIC算法的六阵元圆阵DOA估计matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于MUSIC算法的六阵元圆阵DOA估计matlab仿真. 2.测试软件版本以及运行结果展示 MATLAB2022a版本运行 3.核心程序 ........................................…...
Mysql索引学习
mysql索引-自学版 1 索引语法2 索引类别3 索引原理磁盘IO与预读索引数据结构 B树B树的前生今世B 树代码(进阶) 4 索引使用策略及优化优化索引的几种方法 索引常见面试题面经实战 1 索引语法 索引的语法:创建、修改、增加、删除等操作&#x…...
【MySQL】:高效利用MySQL函数实用指南
🎥 屿小夏 : 个人主页 🔥个人专栏 : MySQL从入门到进阶 🌄 莫道桑榆晚,为霞尚满天! 文章目录 📑前言一. MySQL函数概论二. 字符串函数三. 数值函数四. 日期函数五. 流程函数…...
vue3+electron开发桌面应用,静态资源处理方式及路径问题总结
目录 1、静态资源放到src/assets/目录下 2、静态路径和动态路径的写法 3、编译时vite.config.js的配置...
2024全国水科技大会暨高氨氮废水厌氧氨氧化处理技术论坛(四)
一、会议背景 为积极应对“十四五”期间我国生态环境治理面临的挑战,加快生态环境科技创新,构建绿色技术创新体系,全面落实科学技术部、生态环境部等部委编制的《“十四五”生态环境领域科技创新专项规划》,积极落实省校合作&…...
基于springboot+vue的美食推荐商城
博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 主要内容:毕业设计(Javaweb项目|小程序|Pyt…...
AI推介-多模态视觉语言模型VLMs论文速览(arXiv方向):2024.02.05-2024.02.10
论文目录~ 1.ViGoR: Improving Visual Grounding of Large Vision Language Models with Fine-Grained Reward Modeling2.CLIP-Loc: Multi-modal Landmark Association for Global Localization in Object-based Maps3.Exploring Visual Culture Awareness in GPT-4V: A Compre…...
华为笔记本自带windows11如何改为win10
目录 一、前言 二、遇到问题 三、问题解决 一、前言 新购买的华为笔记本电脑自带windows11系统,虽然是正版系统,但还是希望能重新装Windows10版本。一是我已经习惯此系统,二是该系统上运行的开发工具比较稳定。 二、遇到问题 说干就干&…...
Axios 面试题
Axios 面试题 问题描述: 什么是 Axios?它的主要特点是什么? 答案: Axios 是一个基于 Promise 的 HTTP 客户端库,用于在浏览器和 Node.js 中发送 HTTP 请求。它具有以下主要特点: 支持浏览器和 Node.js 环境…...
速盾:cdn服务器怎么做
CDN(Content Delivery Network)即内容分发网络,是一种通过将内容(如网页、图片、视频等)缓存到离用户较近的服务器上,以提升用户访问速度和减轻源服务器负载的解决方案。在CDN中,CDN服务器是承担…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器
一.自适应梯度算法Adagrad概述 Adagrad(Adaptive Gradient Algorithm)是一种自适应学习率的优化算法,由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率,适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案
在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…...
数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !
我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...
