资讯详情 WADE框架实战:从项目搭建到增删改查与批量导入的完整指南
📅 2026/11/3 13:31:34
简介WADE开发文档面向使用WADE框架进行企业级Java Web开发的中初级开发者系统梳理框架的基本功能与开发步骤帮助读者快速上手项目搭建与版本改造。文档围绕新建项目与开发改造两条主线展开新建项目部分讲解如何借助quickstart工具生成空项目并说明build.properties中项目名称、项目目录、WADELIB包路径、编译省份等配置项的含义开发改造部分按WADE2.52至2.59各版本列出建议改造点涉及JDK版本配置、输入框回车动作控制、集群缓存共享、客户端DataMap与DatasetList数据格式统一、页面命名调整等内容便于读者对照自身框架版本排查兼容性问题。资源包共1个doc文件约3.24MB内容以配置说明与代码片段为主适合作为开发时的查阅手册。目前已有974人学习可供需要理解WADE框架配置体系与升级改造思路的开发者参考。1. 拿到 WADE 开发文档先别急着敲代码这套老框架的增删改查骨架值得拆一遍如果你手上正好有一套基于 WADE 框架的老系统要维护或者接到一个“祖传项目”的二次开发需求这份 WADE 开发文档大概率能帮你省掉几天翻源码的时间。它不是那种从 Hello World 讲起的入门教程而是一份贴着实际业务写的操作手册怎么用 quickstart 生成空项目、怎么配置 build.properties、怎么做增删改查、怎么批量导入导出、怎么处理版本升级后的改造点。整套东西围绕一个核心思路——页面 HTML 加页面 Java 事件加 Java Bean 三层配合数据统一走 DataMap 和 DatasetList。适合谁适合已经有一定 Java Web 基础、需要快速接手 WADE 项目的开发者也适合想搞清楚这套框架数据流转方式的技术负责人。下面我按“搭项目、写业务、避坑、进阶”的顺序把文档里真正能抄作业的部分拆开讲。2. 用 quickstart 起一个干净项目build.properties 里每个参数都别乱填WADE 的新项目不是手动建目录结构而是靠 quickstart 提供的 ant 任务生成。这一步看着简单但配置项填错一个后面编译打包全是连锁反应。我一般会先把 build.properties 当成整个项目的“户口本”来对待逐行确认再执行 ant newproject。2.1 新建项目的四步操作与目录约定文档给的步骤很明确改配置、跑 ant newproject、清理 quickstart 残留、编译运行。真正容易出问题的是第三步和第四步之间的衔接。page.application 和 sidebar.html 里保留的示例页面名和菜单项如果不删干净新项目启动后菜单里会挂着一堆点进去就报错的链接。# 第一步进入 build 目录确认 build.properties 已按新项目改好 cd build # 第二步生成新项目骨架产物落在 build 目录下 ant newproject # 第三步开发阶段编译默认走 ant all ant all # 第四步测试打包发布用 allwar依赖 ant-contrib.jar ant allwar这里要说明的是ant all 和 ant allwar 的区别不只是“开发用”和“发布用”这么简单。all 走的是普通编译流程产物直接落在 web/WEB-INF/classes 下改完代码重新编译就能生效allwar 会打 war 包需要 ant-contrib.jar 支持条件判断和循环任务如果这个 jar 没放进 ant 的 lib 目录allwar 会直接报 taskdef 找不到。常见做法是把 ant-contrib.jar 放到 ANT_HOME/lib 下而不是项目目录里这样换项目也不用重复放。2.2 build.properties 关键参数逐项说明文档里给出的配置片段信息量很大我按“改了会影响什么”的角度重新梳理一遍参数作用填错后的典型现象PROVINCE_NAME编译时选择的省份代码开发用 common打包部署时省份不对页面取不到对应配置PROJECT_NAME项目名称jar 和 war 都以它命名打包产物名字和部署脚本对不上PROJECT_HOME项目根目录.. 表示上级路径解析错编译找不到源码WADELIB_HOMEwadelib 包目录路径编译时依赖库缺失报 ClassNotFoundAPPLANG应用语言编码文档示例为 GB2312中文乱码页面和数据库两头对不上OLD_NAME1/2/3原系统小写、首字母大写、全大写名称替换不彻底残留旧包名NEW_NAME1/2/3新系统对应三种大小写名称同上大小写不统一导致引用失败OLD_NAME 和 NEW_NAME 这三组大小写变体不是冗余设计。Java 包名用小写类名用首字母大写常量或配置键用全大写ant newproject 在替换时会按这三种形态分别匹配。只改一组另外两组就会残留 quickstart 的字样编译时可能不报错但运行到某个反射或配置读取的地方就翻车。还有一个容易忽略的点wadelib 和应用目录最好放在同一级。如果不在同一级build/build.xml 里引入 build_wadelib.xml 的路径要手动改比如改成../../wadelib/build/build_wadelib.xml。这个路径是相对 build 目录算的不是相对项目根目录我第一次配的时候就在这里绕了半小时。!-- build/build.xml 中引入 wadelib 构建文件的位置路径按实际目录关系调整 -- import file${WADELIB_HOME}/build/build_wadelib.xml/上面这行是逻辑示意实际文档要求的是修改 build.xml 中 import 的 file 属性。判断路径对不对有个笨办法在 build 目录下执行 ant -diagnostics 看 basedir再手动拼一下目标文件是否存在。路径存在但 ant 仍报找不到多半是 WADELIB_HOME 里用了相对路径又被二次解析了改成绝对路径先跑通再换回相对路径。2.3 版本升级后的改造点怎么落地文档里列了从 WADE2.5 到 WADE2.59 的建议改造点这些不是可选项而是框架升级后旧写法会出问题的地方。我挑几个影响面大的说。WADE2.52 要求把 web/Buttom.html、view/Buttom.java 改成 Bottom.html、Bottom.javapage.application 里的配置也要同步改。这是拼写修正但旧文件不删、配置不改底部页面直接报错。WADE2.5 去掉了scriptinit();/script这种初始脚本调用方式改成页面载入时按页面名自动生成初始方法比如页面名是 examples.basic.VipcustEdit默认调 initVipcustEidt()也可以通过 wade:Head 的 initScript 参数自定义。如果还按老写法在 HTML 里塞 init()页面加载时不会执行表现为某些初始化逻辑“莫名其妙不生效”。WADE2.53 建议客户端统一用 DataMap 和 DatasetList服务端通过 setAjaxDataset 把数据传到客户端。这个改造的价值在于数据格式和服务端一致客户端拿到 DatasetList 后可以直接操作不用再自己拼 JSON 再解析。对于局部刷新场景这比传统 ajax 取 HTML 片段更轻。WADE2.55 的缓存集群共享需要每个应用更新到 Quickstart2.35 及以上参照 etc/common/ehcache.xml 改造。做法是拿 quickstart 的 ehcache.xml 增量配置加上自己应用的自定义 cache 配置。这里别整份覆盖增量合并才不会丢已有配置。WADE2.56 给输入框组件加了 enterAction 参数回车动作统一用它控制别再在页面里散写 onkeydown。WADE2.59 要求配置 JDK_VERSION1.4 或 1.5这个参数同时决定编译用的 Java 版本和能否部署到 jdk1.4 运行环境。老系统迁移时这个值填错编译能过但部署后类版本不兼容。3. 增删改查四件套HTML、页面 Java、Bean 三层怎么配合WADE 的业务开发有个固定套路HTML 里用 jwcid 绑定组件和事件页面 Java 类接住事件、取参数、调 BeanBean 里用 ExamplesDAO 操作数据。数据统一以文本方式进出数据集时间也直接写成 yyyy-MM-dd HH:mm:ss 这种字符串形式。下面按新增、修改、删除、查看的顺序把每层的写法拆开。3.1 新增数据从页面跳转到序列取值入库新增的入口是一个 PageRedirect 组件点击后跳到编辑页。HTML 写法参考 quickstart 里的 VipcustList.html!-- 列表页的新增入口跳转到编辑页 -- a jwcidwade:PageRedirect sourceognl:condition value新增 classe_bLinkAdd onclickredirectTo(examples.basic.VipcustEdit, null, parameters)/redirectTo 的第一个参数是目标页面名第二个参数是监听方法名第三个是参数集合。新增场景第二个参数传 null因为编辑页打开时不需要预先加载数据。页面 Java 类里接住提交事件把页面数据交给 Bean 处理再把生成的 VIP_ID 传到下页public void createVipcust(IRequestCycle cycle) throws Exception { PageData pd getPageData(); ExamplesBean bean new ExamplesBean(); // 页面数据提交给 Bean 的新增方法返回生成的 VIP_ID String vip_id bean.createVipcust(pd, pd.getData()); // 把主键传到下页供后续查看详情使用 pd.setTransfer(VIP_ID, vip_id); // 操作提示两个按钮分别回到新增页和详情页 redirectToMsg(VIP 客户资料新增成功, new String[] { getPageName(), getPageName() }, new String[] { null, queryVipcust }, new String[] { 继续新增, 查看详情 }); }Bean 里做三件事取序列、补默认字段、插入表。序列用 dao.getSequence(SEQ_VIP_ID) 拿默认字段包括有效标记、默认网别、登录员工、归属部门、归属业务区、归属地州、加入时间和登记时间。这些字段从 pd.getContext() 里取不用页面传。public String createVipcust(PageData pd, IData data) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); String vip_id dao.getSequence(SEQ_VIP_ID); // 用页面提交的客户标识覆盖用户标识和客户标识 data.put(USER_ID, data.get(USECUST_ID)); data.put(CUST_ID, data.get(USECUST_ID)); data.put(CUST_NAME, data.get(USECUST_NAME)); data.put(VIP_ID, vip_id); data.put(REMOVE_TAG, ExampleFactory.VIP_REMOVETAG_VALID); data.put(NET_TYPE_CODE, ExampleFactory.VIP_NETTYPE_DEFAULT); // 以下字段从会话上下文取不依赖页面传参 data.put(IN_STAFF_ID, pd.getContext().getStaffId()); data.put(IN_DEPART_ID, pd.getContext().getDeptId()); data.put(CITY_CODE, pd.getContext().getCityId()); data.put(EPARCHY_CODE, pd.getContext().getEpachyId()); data.put(JOIN_DATE, common.getSysTime()); data.put(IN_DATE, common.getSysTime()); dao.insert(TF_F_CUST_VIP, data); return vip_id; }参数说明pd 是页面数据容器data 是当前提交的数据集dao.insert 的第二个参数可以是单个 IData也可以是 IDataset批量新增时传数据集即可。时间字段直接写字符串框架按文本方式导入不需要转 Date 对象。3.2 修改数据并发场景下必须带状态列做校验修改的坑不在写法在并发。同一个页面打开多次或者多人同时操作你更新之前可能已经有人删了或改了状态。文档明确要求传递关键数据列比如主键加状态列用来识别记录是否还能改。HTML 跳转时用 columns 指定要解析成 URL 参数的列!-- 跳转到编辑页把 info 对象的 VIP_ID 解析成参数传递 -- a jwcidwade:PageRedirect sourceognl:info columnsVIP_ID valueognl:info.VIP_ID onclickredirectTo(examples.basic.VipcustEdit, queryVipcust, parameters)/页面 Java 里取主键和注销标记调 Bean 的更新方法返回 false 说明记录已被动过public void updateVipcust(IRequestCycle cycle) throws Exception { PageData pd getPageData(); String vip_id pd.getParameter(VIP_ID); String remove_tag pd.getParameter(REMOVE_TAG); ExamplesBean bean new ExamplesBean(); // 传入主键和注销标记匹配不到或状态已变则返回 false if (!bean.updateVipcust(pd, pd.getData(), new String[] { vip_id, remove_tag })) common.error(修改失败信息在此操作前已经被操作过); pd.setTransfer(VIP_ID); redirectToMsg(VIP 客户资料修改成功, new String[] { getPageName(), getPageName() }, new String[] { null, queryVipcust }, new String[] { 新增资料, 查看详情 }); }Bean 里用 dao.save 而不是 dao.update这是关键区别。update 会把 data 里所有字段更新表里原有但 data 没指定的字段会被清空save 先查出原记录再用新数据覆盖后更新。只改部分字段时必须用 save除非 data 已经包含全部字段才用 update 换效率。public boolean updateVipcust(PageData pd, IData data, String[] values) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); // 按 VIP_ID 和 REMOVE_TAG 匹配记录匹配成功才更新 return dao.save(TF_F_CUST_VIP, data, new String[] { VIP_ID, REMOVE_TAG }, values); }save 后面跟的两个数组前者是列名后者是对应值。表没有主键或有多个主键但想指定列更新时就靠这组参数。不传的话默认按表结构主键更新。3.3 删除与查看绑定指定列别依赖默认主键删除的 HTML 用 PageSubmit 组件listener 指向页面 Java 的删除方法onclick 里先弹确认框!-- 删除链接queryBox 返回 false 时中止提交 -- a jwcidwade:PageSubmit value删除 listenerognl:listeners.deleteVipcust descVIP 客户资料 classe_bLinkDelete onclickreturn queryBox(this, vips);/页面 Java 取主键和注销标记调 Bean 删除失败时提示记录已被操作public void deleteVipcust(IRequestCycle cycle) throws Exception { PageData pd getPageData(); ExamplesBean bean new ExamplesBean(); if (!bean.deleteVipcust(pd, pd.getParameter(VIP_ID), pd.getParameter(REMOVE_TAG))) common.error(删除失败信息在此操作前已经被操作过); redirectToMsg(VIP 客户资料删除成功); }Bean 里构造一个只含主键和状态列的 DataMap调 dao.delete 并指定匹配列public boolean deleteVipcust(PageData pd, String vip_id, String remove_tag) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); IData data new DataMap(); data.put(VIP_ID, vip_id); data.put(REMOVE_TAG, remove_tag); // 按 VIP_ID 和 REMOVE_TAG 匹配删除避免误删已变更记录 return dao.delete(TF_F_CUST_VIP, data, new String[] { VIP_ID, REMOVE_TAG }); }查看和修改共用跳转方式页面 Java 里取 VIP_ID 查记录查不到就报错public void queryVipcust(IRequestCycle cycle) throws Exception { PageData pd getPageData(); String vip_id pd.getParameter(VIP_ID); if (vip_id ! null) { ExamplesBean bean new ExamplesBean(); IData info bean.queryVipcust(pd, vip_id); if (info null) common.error(您要查看的记录已不存在); setInfo(info); } }Bean 里用 queryByPK 按主键查单条同样可以绑定指定列public IData queryVipcust(PageData pd, String vip_id) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); IData data new DataMap(); data.put(VIP_ID, vip_id); // 默认按主键查也可追加列名数组指定匹配列 return dao.queryByPK(TF_F_CUST_VIP, data); }3.4 批量操作数据集进出自定义批量走 executeBatch批量新增把单条逻辑套进循环最后一次性 insert 数据集public void createVipcusts(PageData pd, IDataset dataset) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); for (int i 0; i dataset.size(); i) { IData data (IData) dataset.get(i); data.put(USER_ID, data.get(USECUST_ID)); data.put(CUST_ID, data.get(USECUST_ID)); data.put(CUST_NAME, data.get(USECUST_NAME)); data.put(VIP_ID, dao.getSequence(SEQ_VIP_ID)); data.put(REMOVE_TAG, ExampleFactory.VIP_REMOVETAG_VALID); data.put(NET_TYPE_CODE, ExampleFactory.VIP_NETTYPE_DEFAULT); data.put(IN_STAFF_ID, pd.getContext().getStaffId()); data.put(IN_DEPART_ID, pd.getContext().getDeptId()); data.put(CITY_CODE, pd.getContext().getCityId()); data.put(EPARCHY_CODE, pd.getContext().getEpachyId()); data.put(JOIN_DATE, common.getSysTime()); data.put(IN_DATE, common.getSysTime()); } // 整个数据集一次插入 dao.insert(TF_F_CUST_VIP, dataset); }批量删除把选中的主键拼成数据集一次 deletepublic void deleteVipcusts(PageData pd, String[] vips) throws Exception { ExamplesDAO dao new ExamplesDAO(pd); IDataset dataset new DatasetList(); for (int i 0; i vips.length; i) { IData data new DataMap(); data.put(VIP_ID, vips[i]); dataset.add(data); } dao.delete(TF_F_CUST_VIP, dataset); }通用的 insert、update、delete 覆盖不了的情况用 executeBatch 自己拼 SQL 和参数。文档里的示例 SQL 有个明显笔误set CUST_NAME ? and CUST_TYPE ?应该是逗号连接照抄会报语法错误我按正确写法给出public void batchUpdateVipcusts(IData param) throws Exception { String[] vip_ids common.getValues(param.get(VIP_ID)); String[] cust_names common.getValues(param.get(CUST_NAME)); String[] cust_types common.getValues(param.get(CUST_TYPE)); // 注意 set 子句用逗号分隔不是 and String sql update TF_F_VIP_CUST set CUST_NAME ?, CUST_TYPE ? where VIP_ID ?; Parameter[] params new Parameter[vip_ids.length]; for (int i 0; i params.length; i) { params[i] new Parameter(); params[i].add(cust_names[i]); params[i].add(cust_types[i]); params[i].add(vip_ids[i]); } executeBatch(sql, params); }参数绑定顺序必须和 SQL 里问号顺序一致错一个位置就是整批数据写错列这种错误不会报异常只会静默写脏数据排查起来很费劲。4. Excel 批量导入配置、页面、Bean 三段式失败数据要能原样导出导入功能是文档里步骤最多的一块从配置到页面到业务逻辑一共七步。核心思路是import 配置定义校验规则export 配置定义失败数据的导出格式页面用 multipart 表单上传Bean 里逐条处理并收集失败集最后把失败集生成 Excel 供下载。4.1 导入配置与失败导出配置的写法差异导入配置放在 import 目录下每个 cell 定义列名、描述、类型、对齐、宽度、是否必填、最大长度sheet descVIP 客户资料 header isshowtrue height300 cell nameUSECUST_ID desc客户标识 type2 align1 width4000 nullableno maxsize16/ cell nameUSECUST_NAME desc客户名称 type1 align1 width6000 nullableno maxsize100/ /header /sheettype 的取值1 字符串、2 数字、3 时间。nullable 为 no 表示必填maxsize 限制输入长度。日期和金额的格式校验也在这里配。失败导出配置放在 export 目录下关键要求是所有列都设成 type1并且加一列 IMPORT_ERROR 显示具体错误sheet nameVIP 客户资料 desc日程信息 header isshowtrue height300 cell nameSCHE_SUBJECT desc日程主题 type1 align1 width6000/ cell nameIMPORT_ERROR desc导入错误 type1 align1 width10000/ /header /sheet为什么全设 type1因为失败数据要和用户原始输入保持一致。如果某列设了 type3 加 formatyyyy-MM-dd HH:mm用户填 2007-03-01导出时会变成 2007-03-01 00:00原始形式就丢了用户拿着失败文件改都不知道原来填的是什么。4.2 页面表单与导入方法的完整链路页面表单必须设 enctypemultipart/form-data否则服务端取不到文件流!-- 上传表单enctype 必须为 multipart/form-data -- form methodpost enctypemultipart/form-data input typefile nameFILE_PATH/ a href# jwcidLinkSubmit listenerognl:listeners.importSchedules value导入 desc日程信息/a !-- 模板下载链接file_name 是下载名file_path 是模板实际路径 -- a hrefattach?file_name日程批量导入模板.xlsfile_path/templet/examples/basic/VipcustTemplet.xls targetprintframe下载模板/a /form页面 Java 里用 ExcelParser.importExcel 解析上传流拿到数据集后交给 Bean 处理失败集非空就生成 Excel 并给出下载链接public void importVipcusts(IRequestCycle cycle) throws Exception { PageData pd getPageData(); // 按导入配置解析 Excel取第一个 sheet 的数据集 IDataset dataset ExcelParser.importExcel(pd, examples/basic/VipcustList.xml, pd.getUploadFile(FILE_PATH).getStream())[0]; ExamplesBean bean new ExamplesBean(); IDataset failds bean.importVipcusts(pd, dataset); if (failds.size() ! 0) { // 失败数据按导出配置生成 Excel返回文件标识 String file_id ExcelParser.writeExcelByImport(pd, examples/basic/VipcustImport.xml, new IDataset[] { failds }); redirectToMsgByPopup(共导入 dataset.size() 条成功 (dataset.size() - failds.size()) 条失败 failds.size() 条请点击[ ExcelParser.getFailedLinkByImport(file_id, 日程批量导入失败列表.xls) ]下载导入失败文件); } else { redirectToMsgByPopup(VIP 客户资料批量导入成功); } }Bean 里逐条处理成功的放 succds失败的补 IMPORT_RESULT 和 IMPORT_ERROR 后放 failds。业务校验比如客户标识是否重复、状态是否合法要自己写框架只做格式校验。提示导入页面必须包含 verifyAll、confrimForm、cinfrimInfo 这类标准验证方法否则后台拿不到数据。这几个方法名文档里拼写不完全统一以 quickstart 里的实际页面为准照抄时别自己改拼写。5. 避坑排查这几个坑我踩过你大概率也会遇到5.1 现象ant allwar 报 taskdef 找不到 ant-contrib原因ant-contrib.jar 没放进 ant 的 lib 目录或者放进了项目目录但 build.xml 没配 taskdef 的 classpath。解决把 jar 放到 ANT_HOME/lib 下执行 ant -diagnostics 确认 ant 能加载到如果必须放项目里在 build.xml 顶部显式声明 taskdef 并指向该 jar。5.2 现象新项目启动后菜单点进去报错原因page.application 和 sidebar.html 里 quickstart 的示例页面名和菜单配置没删干净。解决按文档第三步把这两个文件里保留的示例页面名、菜单项逐条删掉再重新编译。删的时候注意别把框架自身的配置项一起删了只删示例业务相关的。5.3 现象修改数据后部分字段变空原因用了 dao.update 而不是 dao.savedata 里没带的字段被清空。解决只改部分字段时一律用 save确实要用 update先确认 data 包含表里所有需要保留的字段。这个坑在联调阶段最容易被发现但一旦上了生产就是数据事故。5.4 现象导入失败文件里的日期格式和用户填的不一样原因失败导出配置里某列设了 type3 和 format导出时做了格式转换。解决失败导出配置所有列统一 type1不做任何转换另加 IMPORT_ERROR 列显示错误原因。5.5 现象页面初始脚本不执行原因还在用scriptinit();/script的老写法WADE2.5 之后已去掉这种调用方式。解决删掉 HTML 里的 init() 调用改用页面名自动生成的初始方法如 initVipcustEidt()或通过 wade:Head 的 initScript 参数指定方法名。方法没写的话页面载入时会忽略不会报错所以表现为“静默不生效”。6. 进阶技巧用 setAjaxDataset 替代局部刷新把数据格式统一到底WADE2.53 之后建议客户端统一用 DataMap 和 DatasetList服务端通过 setAjaxDataset 把数据传到客户端。这个改造对局部刷新场景收益最明显传统做法是 ajax 取一段 HTML 片段塞进页面数据格式和页面结构耦合新做法是服务端直接传数据集客户端拿到 DatasetList 后自己决定怎么渲染性能和灵活性都更好。具体落地时服务端在页面 Java 里调 setAjaxDataset 把数据集挂上去客户端用对应的 API 取 DatasetList 操作。判断改造范围有个简单标准如果某个 ajax 操作只是刷新一小块区域的数据优先改成数据集传递如果整块区域的结构都要变保留原来的片段刷新反而更省事。验证改造是否生效我一般看两点一是客户端拿到的对象类型是不是 DatasetList二是数据里的时间字段是不是保持了 yyyy-MM-dd HH:mm:ss 的文本形式。这两点都对说明数据格式已经和服务端对齐。另外WADE2.55 的缓存集群共享改造也值得顺手做掉拿 quickstart 的 ehcache.xml 增量合并到自己的 etc/common/ehcache.xml别整份覆盖自定义 cache 配置要保留。还有一个容易漏的细节WADE2.5 规范了 DOM 操作方式获取对象用 getElement(name)、getElements(name)隐藏用 hidden(name|obj)禁用用 disabled(name|obj)禁止直接写 document.getElementById(name).disabled true。因为很多组件不是单个 DOM 对象比如日期组件包含一个文本框和一个图标直接操作只会隐藏文本框图标还在。这个坑在改造老页面时特别常见表现是“禁用了但还能点”。从那以后我每次接手 WADE 项目都先把 build.properties 和 page.application 过一遍再拿一个最简单的增删改查页面跑通全链路确认编译、跳转、数据进出都没问题才开始批量改业务。这套流程看着慢但比改到一半发现项目骨架就是歪的要省时间得多。希望帮到你。本文还有配套的精品资源点击获取