1. 项目概述:从面试题到实战框架的跨越
最近几年,但凡面试自动化测试岗位,尤其是涉及Web UI自动化的,POM(Page Object Model,页面对象模型)几乎成了必考题。面试官上来可能不问你怎么定位元素,而是直接让你谈谈对POM的理解,或者让你在白板上画一个基于POM的框架设计图。我经历过不少这样的场面,也从最初的懵懂到后来能清晰阐述其价值,再到自己主导设计并落地了多个复杂项目的测试框架。我发现,很多朋友对POM的理解还停留在“把页面元素和方法封装到一个类里”的层面,这其实只触及了皮毛。一个健壮、可维护、易扩展的Selenium自动化测试框架,其POM设计远不止于此,它关乎整个测试工程的结构、团队协作的效率以及项目长期维护的成本。
今天,我就结合自己在大厂踩过的坑和积累的经验,抛开那些华而不实的理论,直接聊聊如何从零开始,设计一个真正能在项目中用起来、并且能帮你通过技术面试的Selenium POM框架。我们会从最核心的设计思想讲起,一步步拆解目录结构、基类封装、元素定位策略、用例组织、数据驱动,最后再分享几个高频的面试问题及回答思路。无论你是正在准备面试,还是希望优化手头的测试项目,相信这些实战心得都能给你带来直接的帮助。
2. POM框架的核心设计思想与价值
2.1 为什么大厂都推崇POM?不止是“封装”
很多教程一上来就教你怎么写Page类,但很少深入讲“为什么非得这么设计”。在我看来,POM的核心价值在于分离关注点和降低维护成本。
想象一下没有POM的脚本:你的测试用例里混杂着各种driver.find_element(By.ID, “username”).send_keys(“admin”)。当登录按钮的ID从loginBtn变成submitBtn时,你需要在所有用到这个按钮的用例里逐个修改。这种脚本的脆弱性极高,几乎不可维护。
POM通过将页面元素定位、页面操作行为和测试业务逻辑进行分层,解决了这个问题:
- 元素定位层:集中在Page类中。页面UI一变,你只需要修改这一个文件里的定位器。
- 操作行为层:也在Page类中,封装成诸如
login(username, password)这样的方法。测试用例无需关心如何找到输入框并点击,只需调用这个“业务语言”方法。 - 测试逻辑层:在TestCase中,只包含测试步骤和断言。它读起来就像一份测试用例文档:“打开登录页 -> 输入凭证 -> 点击登录 -> 验证跳转”。
这种分离带来的直接好处是可维护性和可读性的巨大提升。对于面试官而言,他能从你对POM价值的阐述中,判断你是否具备工程化思维,而不仅仅是会写脚本。
2.2 超越基础POM:Page Objects, Page Elements 与 Page Modules
基础POM满足了大部分场景,但在面对复杂页面时,我们还需要更精细的抽象。这里分享两个进阶模式:
1. Page Elements (或叫 Widgets/Components)对于一些在多个页面复用的复杂UI组件,比如导航栏、模态框、日期选择器、表格,不应该在每个Page类里重复编写其定位和操作。应该将其抽象成独立的类。例如,一个ModalDialog类,内部封装了标题、内容、确认按钮、取消按钮的定位,以及confirm()、cancel()等方法。任何页面只要包含这个模态框,就可以将其声明为一个属性。这符合DRY(Don‘t Repeat Yourself)原则,极大提升了代码复用率。
2. Page Modules (业务模块封装)有时,一个完整的业务流程会跨越多个页面。例如“发布文章”流程,可能涉及:文章列表页(点击新建)-> 文章编辑页(填写内容)-> 文章预览页(确认发布)。我们可以创建一个ArticlePublisher模块类,内部组合了ArticleListPage、ArticleEditPage、ArticlePreviewPage的对象,并提供一个publish_article(title, content)的高阶方法。这样,测试用例一行代码就能完成整个流程,逻辑更清晰。
在面试中,如果你能主动提到这些进阶设计,并说明它们解决了“重复代码”和“流程碎片化”的问题,绝对是加分项。
3. 框架目录结构设计与职责划分
一个清晰的目录结构是框架的骨架。下面是一个我经过多个项目迭代后总结出的、比较通用的结构,你可以直接参考:
project_root/ ├── config/ # 配置文件 │ ├── __init__.py │ ├── config.yaml # 全局配置(浏览器、环境URL、超时时间等) │ └── elements.yaml # (可选)非代码方式管理元素定位 ├── drivers/ # 浏览器驱动存放目录 │ └── chromedriver.exe ├── logs/ # 日志文件目录(运行时自动生成) ├── reports/ # 测试报告目录(运行时自动生成) ├── page_objects/ # 页面对象层 │ ├── __init__.py │ ├── base_page.py # 页面基类,封装公共方法 │ ├── common/ # 公共组件/模块 │ │ ├── __init__.py │ │ ├── header.py # 顶部导航栏组件 │ │ └── modal.py # 模态框组件 │ └── pages/ # 具体页面 │ ├── __init__.py │ ├── login_page.py │ └── home_page.py ├── test_cases/ # 测试用例层 │ ├── __init__.py │ ├── conftest.py # Pytest的Fixture配置(核心!) │ ├── test_login.py │ └── test_home.py ├── test_data/ # 测试数据层 │ ├── __init__.py │ └── login_data.yaml ├── utilities/ # 工具层 │ ├── __init__.py │ ├── logger.py # 日志记录器 │ ├── helper.py # 通用辅助函数(截图、等待、数据库操作等) │ └── data_reader.py # 数据读取器(读取YAML/JSON/Excel) └── requirements.txt # Python依赖包列表各目录核心职责解析:
- config/: 存放所有配置。将环境、浏览器、数据库连接等信息与代码分离,是实现“一套代码,多环境运行”的关键。
config.yaml是核心。 - page_objects/: 框架的心脏。
base_page.py是所有页面类的父亲,提供“找元素”、“点击”、“输入”等基础操作的增强版。common/放可复用的UI组件,pages/放具体的业务页面。 - test_cases/: 存放真正的测试用例。
conftest.py是Pytest的魔力所在,用于定义fixture,比如初始化浏览器驱动(driver)、登录状态等,供所有用例使用。 - test_data/:数据驱动测试的核心。将测试用例与测试数据分离。一个登录测试用例,可以搭配多组数据(正确账号、错误密码、空用户名等)运行多次。
- utilities/: 工具箱。封装那些与具体业务无关但测试又需要的功能,比如写日志、拍错误截图、读取文件、发送测试报告邮件等。保证核心的
page_objects和test_cases目录的纯洁性。
面试高频问题:“如果让你设计一个自动化测试框架,你会怎么规划目录结构?” 按照上面的结构阐述,并说明每层的职责和好处,能体现你清晰的工程化思维。
4. 核心实现:BasePage设计与元素定位策略
4.1 打造强大的BasePage基类
BasePage是所有具体页面类的父类,它的质量直接决定了框架的健壮性和易用性。它至少要完成以下几件事:
- 接收并持有driver实例:每个Page对象都必须知道它在哪个浏览器会话中操作。
- 封装增强的“查找”方法:Selenium原生的
find_element在元素未出现时会立即抛异常。我们需要封装一个智能等待的查找方法。 - 封装通用的页面操作:比如点击、输入、获取文本,并融入日志记录和失败截图。
- 提供页面级别的断言和导航:如验证页面标题、URL,以及跳转到其他页面的简便方法。
下面是一个简化但实用的BasePage示例:
# page_objects/base_page.py import logging from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, StaleElementReferenceException from utilities.logger import get_logger from utilities.helper import take_screenshot class BasePage: def __init__(self, driver): self.driver = driver self.logger = get_logger(__name__) # 获取日志记录器 self.wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5, ignored_exceptions=[StaleElementReferenceException]) def find_element(self, locator, timeout=None): """查找单个元素,支持显式等待""" wait = self.wait if timeout is None else WebDriverWait(self.driver, timeout) try: self.logger.info(f"正在查找元素: {locator}") element = wait.until(EC.presence_of_element_located(locator)) self.logger.info(f"元素查找成功: {locator}") return element except TimeoutException: error_msg = f"查找元素超时: {locator}" self.logger.error(error_msg) take_screenshot(self.driver, “element_not_found”) # 失败时自动截图 raise TimeoutException(error_msg) def click(self, locator): """点击元素""" element = self.find_element(locator) try: self.logger.info(f"点击元素: {locator}") element.click() except Exception as e: self.logger.error(f"点击元素失败 {locator}: {e}") take_screenshot(self.driver, “click_failed”) raise def input_text(self, locator, text): """向元素输入文本,先清空""" element = self.find_element(locator) try: self.logger.info(f"向元素 {locator} 输入文本: {text}") element.clear() element.send_keys(text) except Exception as e: self.logger.error(f"输入文本失败 {locator}: {e}") take_screenshot(self.driver, “input_failed”) raise def get_page_title(self): """获取当前页面标题""" return self.driver.title def is_title_contains(self, text): """验证页面标题是否包含指定文本""" return text in self.get_page_title()4.2 元素定位策略与维护技巧
元素定位是UI自动化的基石,也是维护的痛点。除了使用By.ID,By.XPATH等,有几点实战经验:
- 优先级的黄金法则:
ID>Name>CSS Selector>XPath。ID通常最稳定且高效。尽量避免使用绝对XPath(以/开头),它极度脆弱。 - 使用相对XPath或CSS:利用元素属性、文本、层级关系构造相对路径。例如:
//button[contains(@class, ‘btn-primary’)]或//div[@id=‘container’]//input[@type=‘text’]。 - 将定位器集中管理:不要在Page类的方法里硬编码定位字符串。推荐在Page类的顶部,以类属性的形式统一声明。这样一目了然,便于修改。
# page_objects/pages/login_page.py from selenium.webdriver.common.by import By from page_objects.base_page import BasePage class LoginPage(BasePage): # 集中管理定位器 USERNAME_INPUT = (By.ID, “username”) PASSWORD_INPUT = (By.ID, “password”) LOGIN_BUTTON = (By.XPATH, “//button[@type=‘submit’]”) ERROR_MSG_SPAN = (By.CLASS_NAME, “error-message”) def __init__(self, driver): super().__init__(driver) # 页面URL,可用于跳转验证 self.url = “/login” def open(self): self.driver.get(self.base_url + self.url) return self def login(self, username, password): self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) # 返回下一个可能出现的页面对象,如HomePage from .home_page import HomePage return HomePage(self.driver) def get_error_message(self): return self.find_element(self.ERROR_MSG_SPAN).text- 应对动态元素:对于加载缓慢或动态生成的元素,
BasePage.find_element中已经封装了显式等待。对于像Ajax加载后出现的元素,可能需要等待某个特定条件,如元素可点击(EC.element_to_be_clickable)或元素包含特定文本(EC.text_to_be_present_in_element)。
5. 测试用例组织、数据驱动与Fixture设计
5.1 使用Pytest组织测试用例
Pytest是目前Python自动化测试的主流选择,比unittest更简洁灵活。我们的测试用例文件看起来应该非常干净。
# test_cases/test_login.py import pytest from page_objects.pages.login_page import LoginPage class TestLogin: """登录功能测试集""" @pytest.mark.smoke # 使用标记来分类用例,如冒烟测试 def test_login_success(self, driver, base_url): """测试正常登录""" login_page = LoginPage(driver) home_page = login_page.open().login(“valid_user”, “valid_pass”) # 断言:登录成功后应跳转到首页,且首页包含用户信息 assert home_page.is_user_profile_visible(), “登录成功后用户信息未显示” assert “dashboard” in driver.current_url, “未跳转到仪表盘页面” @pytest.mark.parametrize(“username, password, expected_error”, [ (“”, “somepass”, “用户名不能为空”), (“invalid”, “”, “密码不能为空”), (“wrong”, “wrong”, “用户名或密码错误”), ]) def test_login_failure(self, driver, base_url, username, password, expected_error): """测试登录失败的各种情况 - 数据驱动""" login_page = LoginPage(driver) login_page.open().login(username, password) # 断言:应显示正确的错误信息 actual_error = login_page.get_error_message() assert expected_error in actual_error, f”错误信息不符。期望: ‘{expected_error}’, 实际: ‘{actual_error}’”5.2 数据驱动测试的实现
上面的@pytest.mark.parametrize是一种内联的数据驱动方式,适合数据量小、结构简单的场景。对于复杂数据,我们通常将数据放在外部文件(YAML, JSON, Excel)中。
1. 创建数据文件(test_data/login_data.yaml):
success: username: “valid_user” password: “valid_pass” expected_url: “/dashboard” failure_cases: - username: “” password: “somepass” expected_error: “用户名不能为空” - username: “invalid” password: “” expected_error: “密码不能为空” - username: “wrong” password: “wrong” expected_error: “用户名或密码错误”2. 创建数据读取工具(utilities/data_reader.py):
import yaml import json import os class DataReader: @staticmethod def load_yaml(file_path): with open(file_path, ‘r’, encoding=‘utf-8’) as f: return yaml.safe_load(f) @staticmethod def load_json(file_path): with open(file_path, ‘r’, encoding=‘utf-8’) as f: return json.load(f)3. 在测试用例中使用外部数据:
import pytest from utilities.data_reader import DataReader class TestLoginWithExternalData: data_path = os.path.join(os.path.dirname(__file__), ‘..’, ‘test_data’, ‘login_data.yaml’) test_data = DataReader.load_yaml(data_path) @pytest.mark.parametrize(“case”, test_data[“failure_cases”]) def test_login_failure_with_data(self, driver, base_url, case): login_page = LoginPage(driver) login_page.open().login(case[“username”], case[“password”]) assert case[“expected_error”] in login_page.get_error_message()5.3 巧用Pytest Fixture管理测试生命周期
conftest.py是Pytest框架的精华,在这里定义的fixture可以供同一目录及子目录下的所有测试文件使用。它是管理测试前置条件(setup)和清理工作(teardown)的最佳场所。
# test_cases/conftest.py import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options from utilities.logger import get_logger logger = get_logger(__name__) @pytest.fixture(scope=“session”) # 作用域为整个测试会话,只执行一次 def config(): """读取全局配置""" import yaml with open(‘../config/config.yaml’, ‘r’) as f: return yaml.safe_load(f) @pytest.fixture(scope=“function”) # 作用域为每个测试函数,最常用 def driver(config): """初始化浏览器驱动,每个测试用例一个独立的浏览器实例""" browser = config[‘browser’].lower() if browser == “chrome”: options = Options() if config.get(‘headless’, False): options.add_argument(“--headless”) # 无头模式,不打开GUI options.add_argument(“--disable-gpu”) options.add_argument(“--no-sandbox”) options.add_argument(“--window-size=1920,1080”) # 可添加更多选项,如禁用通知、忽略证书错误等 driver = webdriver.Chrome(options=options) elif browser == “firefox”: # 类似地初始化Firefox driver = webdriver.Firefox() else: raise ValueError(f”不支持的浏览器: {browser}”) driver.implicitly_wait(config.get(‘implicit_wait’, 10)) # 隐式等待 logger.info(f”{browser.capitalize()} 浏览器已启动”) yield driver # 将driver对象提供给测试用例使用 # 以下是teardown部分,每个用例结束后执行 logger.info(“测试结束,关闭浏览器”) driver.quit() @pytest.fixture(scope=“function”) def base_url(config): """提供基础URL""" return config[‘base_url’] @pytest.fixture(scope=“function”) def login(driver, base_url): """提供一个已登录状态的fixture""" from page_objects.pages.login_page import LoginPage login_page = LoginPage(driver) home_page = login_page.open().login(config[‘test_user’], config[‘test_pass’]) yield home_page # 将登录后的首页对象提供给用例 # 如果需要,可以在这里执行登出操作 # home_page.logout()在测试用例中,你只需要将driver,base_url,login这些fixture名称作为参数传入,Pytest会自动注入对应的对象或值。这使得用例代码极其简洁,且保证了环境的一致性和隔离性。
6. 日志、报告与异常处理机制
6.1 构建清晰的日志系统
没有日志的自动化框架就像在黑暗中调试。一个好的日志系统能帮你快速定位问题。使用Python标准的logging模块进行封装。
# utilities/logger.py import logging import os from datetime import datetime def get_logger(name, log_level=logging.INFO): # 创建logger logger = logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加handler if not logger.handlers: # 创建控制台handler ch = logging.StreamHandler() ch.setLevel(log_level) # 创建文件handler,按日期生成日志文件 log_dir = “logs” os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, f”test_{datetime.now().strftime(‘%Y%m%d’)}.log”) fh = logging.FileHandler(log_file, encoding=‘utf-8’) fh.setLevel(log_level) # 定义格式 formatter = logging.Formatter(‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’) ch.setFormatter(formatter) fh.setFormatter(formatter) # 添加handler到logger logger.addHandler(ch) logger.addHandler(fh) return logger在BasePage和conftest.py中引入这个logger,在关键操作(如查找元素、点击、输入)和生命周期事件(如启动、关闭浏览器)处记录信息。当测试失败时,查看日志文件就能清晰地看到操作序列和错误发生的位置。
6.2 生成美观的测试报告
测试报告是向团队展示自动化成果的重要方式。pytest-html和Allure是两个主流选择。
pytest-html:简单易用,生成一个独立的HTML文件。
pytest test_cases/ --html=reports/report.html --self-contained-html在
conftest.py中,可以添加hook函数来增强报告,比如把失败截图嵌入到报告中:@pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == “call” and report.failed: # 假设driver fixture的名字是‘driver’ if “driver” in item.fixturenames: driver = item.funcargs[“driver”] screenshot_path = take_screenshot(driver, report.nodeid.replace(“::”, “_”)) # 将截图路径添加到html报告extra中 if hasattr(report, “extra”): report.extra.append(pytest_html.extras.image(screenshot_path, ‘Failure Screenshot’))Allure:功能强大,报告美观,支持趋势分析、用例分类、附件(截图、日志)等。需要额外安装Java和Allure命令行工具。
pytest test_cases/ --alluredir=./allure-results allure serve ./allure-results # 生成并打开本地报告在代码中,可以使用
@allure装饰器添加丰富的描述、步骤和严重等级,使报告更具可读性。
6.3 健壮的异常处理与失败重试
自动化测试运行在多变的环境里,网络波动、资源加载慢都可能导致偶发性失败。我们需要让框架更“智能”。
- 封装操作,自动截图:正如我们在
BasePage.click和input_text方法中做的,任何操作失败时,除了记录日志,立即截取当前屏幕快照。截图文件名最好包含时间戳和用例名,便于追溯。 - 使用重试机制:对于某些偶发性的元素查找失败或断言失败,可以引入重试。Pytest有
pytest-rerunfailures插件。
运行测试时指定重试次数:pip install pytest-rerunfailures
或者在pytest --reruns 2 --reruns-delay 1 # 失败后重试2次,每次间隔1秒conftest.py中全局配置:
注意:重试应谨慎使用,它可能掩盖真正的代码缺陷或环境问题。通常只用于最外层的、不稳定的UI交互。def pytest_configure(config): config.option.reruns = 2 config.option.reruns_delay = 1
7. 大厂面试经验与高频问题剖析
最后,结合我作为面试官和被面试者的经验,聊聊围绕Selenium POM框架设计,面试官常问的几个问题及回答要点。
问题一:“请简述你对POM设计模式的理解,它解决了什么问题?”
- 回答要点:
- 定义:POM是一种设计模式,将Web UI的每一个页面抽象成一个类(Page Class),页面上的元素定位器和操作这个元素的方法都封装在这个类里。
- 核心思想:分离测试对象(页面元素)、测试操作(业务方法)和测试用例(测试逻辑)。
- 解决的问题:
- 代码复用:相同的页面操作逻辑只需写一次。
- 易于维护:UI变更时,只需修改对应的Page类,无需改动大量测试用例。
- 可读性强:测试用例用接近自然语言的业务方法编写,清晰易懂。
- 减少冗余:避免了测试脚本中遍布的、重复的元素定位代码。
问题二:“你在设计POM框架时,如何管理页面元素定位器?”
- 回答要点:
- 集中声明:在Page类顶部,以类变量(常量)形式统一声明所有定位器(如
USERNAME_INPUT = (By.ID, “username”))。 - 优先策略:遵循ID > Name > CSS Selector > XPath的优先级,尽量使用稳定、唯一的属性。
- 应对动态元素:使用显式等待(WebDriverWait)处理动态加载的元素,而不是
time.sleep。 - 进阶方案:对于超大型项目,可以考虑将定位器与代码分离,存储于YAML或JSON文件中,通过工具类读取。但这会引入额外的复杂度,需权衡利弊。
- 集中声明:在Page类顶部,以类变量(常量)形式统一声明所有定位器(如
问题三:“你的框架如何实现数据驱动测试?”
- 回答要点:
- 概念:将测试数据从测试脚本中分离出来,使得同一套测试逻辑可以用多组数据执行。
- 实现方式:
- Pytest内联:使用
@pytest.mark.parametrize装饰器,适合数据量少、结构简单的场景。 - 外部文件:将测试数据存储在YAML、JSON、Excel或CSV文件中。框架中编写对应的数据读取器(如
DataReader类)。 - 数据库:对于需要从数据库获取动态数据的场景,可以连接数据库查询。
- Pytest内联:使用
- 结合用例:在测试方法中,接收外部数据作为参数,实现业务逻辑与数据的解耦。
问题四:“如何保证自动化测试的稳定性和可靠性?”(综合性问题)
- 回答要点(从框架设计角度):
- 元素定位策略:使用稳定、唯一的定位方式,并封装智能等待,避免因元素未加载完成导致的失败。
- 异常处理与日志:每个关键操作都有
try-except包裹,记录详细日志,失败时自动截图,便于快速排查。 - 测试数据隔离:使用独立的测试账号和数据,避免测试间相互干扰。用例执行前后做好数据清理(setup/teardown)。
- 环境隔离:使用
fixture(如driver)为每个测试用例提供独立的浏览器实例或会话。 - 失败重试机制:对偶发性失败(如网络超时)配置合理的重试策略。
- 持续集成:将框架接入CI/CD(如Jenkins, GitLab CI),定时或触发式运行,及早发现问题。
问题五:“如果页面频繁变动,你的框架如何应对以降低维护成本?”
- 回答要点:
- POM本身:已经将变动隔离在Page类中,这是最根本的应对。
- 组件化:将高频变动的公共组件(如弹窗、导航)单独抽象成
Component类,一处修改,处处生效。 - 定位器策略:与开发团队约定,为关键测试元素添加稳定的
>
一文看懂 HarmonyOS 6.1.1 的 Canvas 抗锯齿开关能力
先确定它在 Canvas 能力体系中的位置HarmonyOS 6.1.1 为 CanvasRenderingContext2D 与 OffscreenCanvasRenderingContext2D 增加 antialias 属性,用于临时开关文本抗锯齿。只看接口形态,它只是上下文对象上的一个布尔值;放回二维绘图体系&…
电子合同平台进入下半场:从签署效率工具到履约管理基础设施的行业演进
国内电子合同行业在过去十年经历了三次价值重心的迁移。 第一个阶段是"合法化"阶段,核心命题是电子签名是否具有法律效力,行业的注意力集中在数字证书、CA认证和司法采信等合规问题上。第二个阶段是"效率化"阶段,电子签章…
Ubuntu 18.04安装Nvidia显卡驱动:从原理到实战的完整避坑指南
1. 项目概述与核心痛点在Linux环境下,尤其是Ubuntu这类发行版上安装Nvidia显卡驱动,对于很多从Windows或macOS转过来的开发者、研究人员和爱好者来说,几乎可以算作一个“成人礼”。我见过太多人,包括我自己早期,在这个…
Linux软链接深度解析:从原理到实战应用
1. 项目概述:软链接,Linux世界的“快捷方式”在Linux世界里混,文件系统操作是基本功。而ln命令,特别是用来创建软链接(也叫符号链接)的那个ln -s,绝对是你绕不开的一个工具。你可以把它理解成Wi…
如何快速解决C盘爆红问题:WindowsCleaner完整指南
如何快速解决C盘爆红问题:WindowsCleaner完整指南 【免费下载链接】WindowsCleaner Windows Cleaner——专治C盘爆红及各种不服! 项目地址: https://gitcode.com/gh_mirrors/wi/WindowsCleaner 你的Windows电脑C盘是否经常显示红色警告࿰…
Fusion 360模型编辑进阶指南:从参数化到直接建模实战
1. 项目概述:从“会用”到“精通”的必经之路如果你是一名创客,或者正在从3D打印、手工制作转向更专业的数字制造领域,那么“编辑模型”这个动作,几乎是你每天都要面对的日常。我们常常会遇到这样的场景:网上下载的模型…