ASP.NET MVC 中集成 AngularJS的应用例子

作者:袖梨 2022-06-25

介绍

当涉及到计算机软件的开发时,我想运用所有的最新技术。例如,前端使用最新的 JavaScript 技术,服务器端使用最新的基于 REST 的 Web API 服务。另外,还有最新的数据库技术、最新的设计模式和技术。

当选择最新的软件技术时,有几个因素在起作用,其中包括如何将这些技术整合起来。过去两年中,我最喜欢的一项技术就是设计单页面应用(SPA)的 AngularJS。作为一个微软stack开发者,我也是使用 ASP.NET MVC 平台实现 MVC 设计模式和并进行研究的粉丝,包括它的捆绑和压缩功能以及实现其对 RESTful 服务的 Web API 控制器。

为了兼得两者,本文介绍了在 ASP.NET MVC 中集成 AngularJS 的两全其美的方案。

由于本文篇幅较长,故会分为3篇,分别进行介绍。

概述


本文中示例的 Web 应用程序将有三个目标:

在前端页面中实现 AngularJS 和 JavaScript AngularJS 控制器
使用微软的 ASP.NET MVC 平台来建立、引导并捆绑一个应用
根据功能模型的需求,动态的加载 AngularJS 的控制器和服务
本文的示例应用程序将包含三个主要文件夹:关于联系和索引的主文件夹、允许你创建,更新和查询客户的客户文件夹、允许你创建,更新和查询产品的产品文件夹。

除了使用 AngularJS 和 ASP.NET MVC,这个应用程序也将实现使用微软的 ASP.NET Web API 服务来创建 RESTful 服务。微软的实体框架将用于生成并更新一个 SQL Server Express 数据库。

此应用程序也将用到一些使用 Ninject 的依赖注入。此外,也会运用流畅的界面和 lambda 表达式,来合并使用称为 FluentValidation的.NET 的小型验证库,用于构建驻留在应用业务层的验证业务规则。

 

AngularJS VS ASP.NET Razor 视图
几年来,我一直在使用完整的 Microsoft ASP.NET MVC 平台来开发 Web 应用程序。相比于使用传统的 ASP.NET Web 窗体的 postback 模型, ASP.NET MVC 平台使用的是 Razor 视图。 这带来的是:适当的业务逻辑、数据和表示逻辑之间关注点的分离。在使用它的约定优于配置和简洁的设计模式进行 MVC 开发之后,你将永远不会想回过头去做 Web 窗体的开发。

 

ASP.NET MVC 平台及其 Razor 视图引擎,不但比 Web 窗体简洁,还鼓励和允许你将 .NET 服务器端代码和样式混合。在 Razor 视图中的 HTML 混合的 .NET 代码看起来像套管代码。另外,在 ASP.NET MVC 模式下,一些业务逻辑是可以被最终写入在 MVC 的控制器中。在MVC控制器中,写入代码来控制表示层中的信息,这是很有诱惑力的。

AngularJS 提供了以下对微软 ASP.NET MVC Razor 视图的增强功能:

AngularJS 视图是纯 HTML 的
AngularJS 视图被缓存在客户端上以实现更快的响应,并在每次请求不产生服务器端响应
AngularJS 提供了一个完整的框架,编写高质量的客户端 JavaScript 代码
AngularJS 提供了 JavaScript 控制器和 HTML 视图之间的完全分离
 

ASP.NET MVC 捆绑和压缩
捆绑和压缩是两种你可以用来缩短 Web 应用程序的请求负载时间的技术。这是通过减少对服务器的请求数量和减小请求规模,来实现缩短请求负载时间的(如 CSS 和 JavaScript)。压缩技术通过复杂的代码逻辑也使得别人更难的侵入你的 JavaScript 代码。

当涉及到捆绑技术和 AngularJS 框架时,你会发现捆绑和压缩过程中会自动使用 Grunt 和 Gulp 之类的框架,Grunt 和 Gulp 技术是一种流行的 web 库并配有插件,它允许你自动化你的每一项工作。

如果你是一个微软开发者,你可以使用它们在 Visual Studio 中一键式发布你的 Web 应用,而不用学习使用任何第三发工具和库类。幸运的是,捆绑和压缩是 ASP.NET 4.5 ASP.NET 中的一项功能,可以很容易地将多个文件合并或捆绑到一个文件中。你可以创建 CSS,JavaScript 和其他包。较少的文件意味着更少的 HTTP 请求,这也可以提高第一个页面的加载性能。

 

使用 RequireJS 来实现 MVC 捆绑的动态加载
在开发 AngularJS 单页的应用程序时,其中有一件事情是不确定的。由于应用开始时会被引导和下载,所以在主页面索引时,AngularJS 会请求所有的 JavaScript 文件和控制器。对于可能包含数百个 JavaScript 文件的大规模应用,这可能不是很理想。因为我想使用 ASP.NET 的捆绑来加载所有的 AngularJS 控制器。一旦开始索引,一个 ASP.NET 捆绑中的巨大的挑战将会出现在服务器端。

为了实现示例程序动态地绑定 ASP.NET 文件包,我决定用 RequireJS JavaScript 库。RequireJS 是一个众所周知的 JavaScript 模块和文件加载器,最新版本的浏览器是支持 RequireJS 的。起初,这似乎是一个很简单的事情,但随着时间的推移,我完成了大量的代码的编写,却并没有解决使用服务器端 rendered bundle 与客户端 AngularJS 等技术的问题。

最终,在大量的研究和反复试验和失败后,我想出了少量代码却行之有效的解决方案。

本文的接下来部分将会展示,在 ASP.NET MVC 中集成 AngularJS 的过程。

 

创建 MVC 项目并安装 Angular NuGet 包
为了开始示例应用程序,我通过在 Visual Studio 2013 专业版中选择 ASP.NET Web 应用程序模板来创建一个 ASP.NET MVC 5 Web 应用程序。之后,我选择了 MVC 工程并在应用中会用到 MVC Web API 添加文件夹和引用。下一步是选择工具菜单中的“管理 NuGet 包的解决方案”,来下载并安装 NuGet AngularJS。

对于此示例应用程序,我安装了所有的以下的 NuGet 包:

AngularJS - 安装整个 AngularJS 库
AngularJS UI - AngularJS 框架的伙伴套件UI工具和脚本。
AngularJS UI引导 - 包含一组原生 AngularJS 指令的引导标记和CSS
AngularJS 块UI - AngularJS BlockUI 指令,块状化 HTTP 中的请求
RequireJS - RequireJS 是一个 JavaScript 文件和模块加载
Ninject – 提供了支持 MVC 和 MVC Web API 支持的依赖注入
实体框架 - 微软推荐的数据访问技术的新应用
流畅的验证 - 建立验证规则的 .NET 验证库。
优美字体- CSS 可立即定制的可升级的矢量图标
NuGet 是一个很好的包管理器。当你使用 NuGet 安装一个软件包,它会拷贝库文件到你的解决方案,并自动更新项目中的引用和配置文件。如果你删除一个包, NuGet 会让所有删除过程不会留下任何痕迹。

 

优美的URLS
对于此示例应用程序,我想在浏览器的地址栏中实现优美的网址。默认情况下,AngularJS 会将 URL 用#标签进行路由:

例如:

http://localhost:16390/
http://localhost:16390/#/contact
http://localhost:16390/#/about
http://localhost:16390/#/customers/CustomerInquiry
http://localhost:16390/#/products/ProductInquiry
通过转向 html5Mode 和设置基本的 URL,可以很方便的清除 URLS 并去除 URL 中的#。在 HTML5 模式下,AngularJS 的$位置服务会和使用 HTML5 History API 的浏览器 URL 地址进行交互。HTML5 History API 是通过脚本来操作浏览器历史记录的标准方法,以这点为核心,是实现单页面应用的重点。

要打开 html5Mode,你需要在 Angular 的配置过程中,将 $locationProviderhtml5Mode 设置为 true,如下所示:

// CodeProjectRouting-production.js
angular.module("codeProject").config('$locationProvider', function ($locationProvider) {
    $locationProvider.html5Mode(true);
}]);
当你使用 html5Mode 配置 $locationProvider 时,你需要使用 href 标记来指定应用的基本 URL。基本 URL 用于在整个应用程序中,解决所有相对 URL 的问题。你可以在应用程序中设置,如下所示的母版页的 header 部分的基本 URL:






对于示例应用程序,我以程序设置的方式将基本 URL 存储在 Web 配置文件中。这是一种最好的方式使得基本 URL 成为一种配置,这样能够让你根据环境、配置或者你开发的应用的站点的情况,来将基本 URL 设定为不同的值。此外,设置基本 URL 时,要确保基本 URL 以“/”为结尾,因为基本 URL 将是所有地址的前缀。


psettings>


打开 html5Mode 并设置基本 URL 后,你需要以以下优美的 URL 作为结束:

http://localhost:16390/
http://localhost:16390/contact
http://localhost:16390/about
http://localhost:16390/customers/CustomerInquiry
http://localhost:16390/products/ProductInquiry
 

目录结构与配置
按照惯例,一个 MVC 项目模板要求所有的 Razor 视图驻留在视图文件夹中; 所有的 JavaScript 文件驻留在脚本文件夹; 所有的内容文件驻留在内容文件夹中。对于此示例应用程序,我想将所有的 Angular 视图和相关的 Angular JavaScript 控制器放入相同的目录下。基于 Web 的应用程序会变得非常大,我不想相关功能以整个应用程序的目录结构存储在不同文件夹中。

 

在示例应用程序,会出现两个 Razor 视图被用到,Index.cshtml 和 _Layout.cshtml 母版页布局,这两个 Razor 视图将用于引导和配置应用程序。应用程序的其余部分将包括 AngularJS 视图和控制器。

对于示例应用程序,我在视图文件夹下创建了两个额外的文件夹,一个客户的子文件夹,一个产品的子文件夹。所有的客户的 Angular 视图和控件器将驻留在客户子文件夹中,所有的产品的 Angular 视图和控件器将驻留在产品子文件夹中 。

由于 Angular 视图是 HTML 文件,而 Angular 控制器是 JavaScript 文件,从 Views 文件夹到浏览器,ASP.NET MVC 必须被配置为允许 HTML 文件和 JavaScript文 件进行访问和传递。这是一个 ASP.NET MVC 默认的约定。幸运的是,你可以通过编辑视图文件下的 web.config 文件并添加一个 HTML 和 JavaScript 的处理器来更改此约定,这将会使这些文件类型能够被送达至浏览器进行解析。





type="System.Web.StaticFileHandler"/>

type="System.Web.StaticFileHandler"/>

 
应用程序版本自动刷新和工程构建
对于此示例应用程序,我想跟踪每一次编译的版本和内部版本号,在属性文件夹下使用 AssemblyInfo.cs 文件的信息测试并发布这个应用。每次应用程序运行的时候,我想获得最新版本的应用程序和使用的版本号,以实现最新的 HTML 文件和 JavaScript 文件生成时,帮助浏览器从缓存中,获取最新的文件来替换那些旧文件。

对于这种应用,我使用的 Visual Studio 2013 专业版,这让一切变得简单,我为 Visual Studio2013 专业版下载了一个自动版本的插件

https://visualstudiogallery.msdn.microsoft.com/dd8c5682-58a4-4c13-a0b4-9eadaba919fe

它会自动刷新 C# 和 VB.NET 项目的版本。将安装插件下载到名为自动版本设置的工具菜单中。该插件自带了配置工具,它允许你配置主要和次要版本号,以便每次编译时,自动的更新 AssemblyInfo.cs 文件。目前,这个插件只是在 Visual Studio 2013 专业版中支持,或者你也可以手动更新版本号或使用类似微软的 TFS 以持续构建和配置管理环境的方式,来管理你的版本号。

 

下面是一个使用更新的 AssemblyVersion 和 AssemlyFileVersion 号的示例,这个示例在版本编译之后会通过插件自动地进行更新。


// AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("CodeProject.Portal")]
[assembly: AssemblyProduct("CodeProject.Portal")]
[assembly: AssemblyCopyright("Copyright © 2015")]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("1d9cf973-f876-4adb-82cc-ac4bdf5fc3bd")]
// Version information for an assembly consists of the following four values:

// Major Version
// Minor Version
// Build Number
// Revision

// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("2015.9.12.403")]

 
使用 Angular 视图和控制器更换联系我们和关于 Razor 视图
要想使用 MVC 工程,首先要做的事情之一就是使用 AngularJS 视图和控制器来更换联系我们和关于 Razor 视图。这是一个很好的起点来测试你的配置是否能够使 AngularJS 正常建立并运行。随后如果不需要这些页面,你可以删除关于和联系我们的视图和控制器。

 

AngularJS 的这种创建控制器的方式是通过注入 $scope 实现的。示例应用程序的视图和控制器使用“controller as”语法。此语法并非使用控制器中的 $scope,而是简化你的控制器的语法。当你声明一个“controller as”语法的控制器时,你会得到该控制器的一个实例。

 

使用“controller as”语法,你的所有的连接到控制器(视图模式)的属性必须以你视图的别名作为前缀。在下面的视图代码片段,属性标题前面就加上了“VM”的别名。



  

当控制器构造函数被调用时,使用“controller as”的语法,叫做“this”的控制器示例就会被创建。不需要使用 Angular 提供的 $scope 变量,你只需要简单的声明一个 vm 变量并分配“this”给它。所有被分配给 vm 对象的变量都会替换掉 $scope。有了分配给控制器功能的示例的变量,我们就可以使用这些别名并访问这些变量。

此外,所有示例应用程序中的控制器都是使用“use strict”JavaScript 命令以一种严格的模式运行的。这种严格模式可以更容易地编写“安全”的 JavaScript 代码。严格模式将此前“不严格的语法”变成了真正的错误。作为一个例子,在一般的 JavaScript 中,错误输入变量名称会创建一个新的全局变量。在严格模式下,这将抛出一个错误,因此无法意外创建一个全局变量。


// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
{
    "use strict";
    var vm = this;

    this.initializeController = function () {
        vm.title = "About Us";
    }
}]);

如前所述,在 MVC Razor 视图中使用 AngularJS 视图和控制器的优势之一,就是 Angular 提供了很好的机制来编写高质量的 JavaScript 模块、一种纯 HTML 视图和 JavaScript 控制器之间的完全分离的编码方式。你不再需要使用 AngularJS 双向数据绑定技术来解析浏览器的文件对象模型,这也就使得你能够编写单元测试的 JavaScript 代码。

作为一个注脚,您将在 aboutController 看到一个名为 register.controller 的方法。在本文的后面,你会看到注册方法是从哪儿来的和它用来做什么。

 
主页索引的 Razor 视图和 MVC 路由
ASP.NET MVC 中集成 AngularJS 的一件有趣的事情,就是应用程序实际上是如何启动和实现路由的。当你启动应用程序时,ASP.NET MVC 将会以如下默认的方式进入并查看路由表:


// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace CodeProject.Portal
{
    publicclass RouteConfig
    {
        publicstaticvoid RegisterRoutes(RouteCollection routes)
        {
          routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
          routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
          );
        }
    }
}

应用开始时,以上外装配置的 MVC 路由表中的配置,会将应用路由到 MVC Home 主控制器,并执行主控制器中的索引方法。这样会以 MVC 默认工程模板的形式,将 Index.cshtml MVC Razor 视图传递到用户输出的主页面内容中。

这个应用程序的目标是使用 Angular 视图取代所有的 MVC 视图。但问题是,甚至在 AngularJS 被启动之前,主页的 Razor 视图索引就已经被执行和注入了 _Layout.cshtml 主页面中。

自从我决定,将主页面改为 AngularJS 视图,我就使用包含 AngularJS ng-view 标签的 div 标签删除了索引 Razor 视图的所有内容。



该 AngularJS ngView 标签是一个指令,能以一种将当前路由的模板渲染成主页面布局的方式补充 $route service。我有两个选择,要么直接嵌入 NG-View 代码到母版页 _Layout.cshtml 或使用 Razor 视图将它注入到母版页。我决定简单地从索引 Razor 视图中注入标签。本质上,索引 Razor 视图在应用程序的引导过程中被简单的使用,并且在应用程序启动后不会被引用。

一旦应用程序被引导并开始启动,AngularJS 将会执行自己的路由系统并以路由表中配置来执行自己的默认路由。基于这一点,我创建了一个单独 AngularJS index.html 和主页的 IndexController.js 文件。



{{vm.title}}


当视图加载时,索引 Angular 视图将会通过 ng-init 指令来执行索引控制器的初始化功能。


// indexController.js
angular.module("codeProject").register.controller('indexController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
        vm.title = "Home Page";
    }
}]);

 
RouteConfig.cs
当开发一个 AngularJS 应用时,首先将会发生的一件事,就是你需要先开发一个像驻留在路由文件中的 CustomerInquiry 一样的页面

/Views/Customers/ CustomerInquiry

当你在 HTML 页面寻找这个视图时,点击 Visual Studio 中的运行按钮来直接执行这个页面,MVC 将会执行并尝试去查找一个用于客户路由的 MVC 控制器和视图。将会发生的是,你会获得一个叫做找不到该路由的视图或控制器的错误。

你当然会遇到这个错误,因为/View/Customers/CustomerInquiry的路由是个 Angular 路由,而不是 MVC 路由。MVC 并不知道这个路由。如果你还想直接运行这个页面,则需要解决这一问题,给 MVC 路由表增加另外的路由以便告诉 MVC 将所有的请求路由到 MVC 主控制器,并渲染Razor 视图、通过路由引导这个应用。

由于我有三个视图文件夹,主文件夹、客户文件夹和产品文件夹,我增加了一下的 MVC 路由配置类以便将所有的请求路由到主/索引路由中。当应用程序运行时点击 F5,同样也会进入 MVC 路由表。就 Angular 和单页面如何运行而言,当你点击 F5 时,基本上就是重启了 AngularJS 应用。

有了这些额外的路由,现在就可以直接执行 AngularJS 路由了。你可以在 MVC 路由表中以一种通配符的路由来处理你的路由,但我更愿意使用明确的路由表,并使得 MVC 拒绝所有无效的路由。

要记住的基本的事情是,MVC 路由将会在 AngularJS 启动之前发生,一旦引导开始,AngularJS 将会接管所有以后路由请求。


// RouteConfig.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace CodeProject.Portal
{
    publicclass RouteConfig
    {
        publicstaticvoid RegisterRoutes(RouteCollection routes)
        {

             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
             routes.MapRoute(
             name: "HomeCatchAllRoute",
             url: "Home/{*.}",
             defaults: new { controller = "Home", action = "Index",
             id = UrlParameter.Optional }
             );
             routes.MapRoute(
             name: "CustomersCatchAllRoute",
             url: "Customers/{*.}",
             defaults: new { controller = "Home", action = "Index",
             id = UrlParameter.Optional }
             );

             routes.MapRoute(
             name: "ProductsCatchAllRoute",
             url: "Products/{*.}",
             defaults: new { controller = "Home", action = "Index",
             id = UrlParameter.Optional }
             );

             routes.MapRoute(
             name: "Default",
             url: "{controller}/{action}/{id}",
             defaults: new { controller = "Home", action = "Index",
             id = UrlParameter.Optional }
          );
       }
   }
}

 
$ controllerProvider 和动态加载控制器
当示例应用程序启动时,该应用程序将会预加载应用程序的核心控制器和服务。这包括 Home 目录中的所有控制器和应用程序的共享服务。

此应用程序的共享服务,将在所有模块中执行- 包括一个 Ajax 服务和提醒服务。如前所述,此应用程序具有三个功能模块:基本的关于、联系我们和主页的模块、一个客户模块和产品模块。

由于此应用程序可随时间而增长,我不希望该在应用程序的配置和引导阶段中,预加载所有的功能模块。应用程序启动后,我仅希望当用户请求时,再加载这些控制器和产品模块。

默认情况下,AngularJS 被设计为预加载所有的控制器。一个典型的控制器看起来这样:


// aboutController.js
angular.module("codeProject").controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
        vm.title = "About";
    }
}]);

如果在配置阶段之后,你尝试动态加载上述控制器,将会收到一个 Angular 错误。你需要做的是使用 $controllerProvider 服务器在配置阶段之后,动态地加载控制器。Angular 使用 $controllerProvider 服务来创建新的控制器。这种方法允许通过注册方法来实现控制器注册。


// aboutController.js
angular.module("codeProject").register.controller('aboutController',
['$routeParams', '$location', function ($routeParams, $location) {
"use strict";
var vm = this;
this.initializeController = function () {
         vm.title = "About";
     }
}]);

上述有关控制器被修改为执行 $controllerProvider 的寄存器方法。为了使这种注册方法有效,必须在配置阶段配置这种注册。下面的代码片段在应用程序启动之后,使用了 $controllerProvider 来使注册方法有效。在下面的例子中,提供了一种用于注册和动态加载两个控制器和服务的注册方法。如果你愿意,也可以包括 Angular 全部库和指令的注册功能。


// CodeProjectBootStrap.js
(function () {
var app = angular.module('codeProject', ['ngRoute', 'ui.bootstrap', 'ngSanitize', 'blockUI']);

app.config(['$controllerProvider', '$provide', function ($controllerProvider, $provide) {
        app.register =
        {
             controller: $controllerProvider.register,
             service: $provide.service
        }
    }
}

以上是如何在 ASP.NET MVC 中集成 AngularJS 的第一部分内容,后续内容会在本系列的后两篇文章中呈现,敬请期待!

通过第一部分内容的学习,相信大家已经对实现在 ASP.NET MVC 中集成 AngularJS 的基本思路有所了解。当我们在进行 ASP.NET MVC 和 AngularJS 开始时,还可以借助开发工具来助力开发过程。ASP.NET MVC开发时,可以借助 ComponentOne Studio ASP.NET MVC 这一款轻量级控件,它与 Visual Studio 无缝集成,完全与 MVC6 和 ASP.NET 5.0 兼容,将大幅提高工作效率;AngularJS 开发时,可以借助 Wijmo 这款为企业应用程序开发而推出的一系列包含 HTML5 和 JavaScript 的开发控件集,无论应用程序是移动端、PC端、还是必须要支持IE6,Wijmo 均能满足需求。

ASP.NET 捆绑和压缩
CSS 和 JavaScript 的捆绑与压缩功能是 ASP.NET MVC 最流行和有效的特性之一。捆绑和压缩降低了 HTTP 请求和有效载荷的大小,结果是可以更快和更好的执行 ASP.NET MVC 的网站。有许多可以减少 CSS 和 JavaScript 合并的大小的方法。

捆绑可以很容易地将多个文件合并或捆绑到一个文件中。您可以创建 CSS,JavaScript 和其他包。压缩可以优化脚本和 CSS 代码,如去除不必要的空格和注释,缩短变量名到一个字符。由于捆绑和压缩降低你的 JavaScript 和 CSS 文件的大小,发送的 HTTP 的字节也会显著降低。

当配置包文件时,你需要考虑一个捆绑策略以及如何组织你的包文件。下面的 BundleConfig 类是内置的 ASP.NET 捆绑功能的配置文件。在 BundleConfig 类,我决定通过功能模块来组织我的文件。我为工程中的每一个文件设置了一个独立的捆绑,包括对脚本的单独捆绑,Angular 的核心文件,共享的 JavaScript 文件和主目录单,客户目录和产品目录。

我创建了客户和产品目录的独立包,带着这种想法,当用户请求应用程序的这些源文件时,应以将会动态的加载这些捆绑。由于 AngularJS 是一个纯客户端框架,可以动态加载 ASP.NET 包和服务器端技术,所以这两项技术相结合,成为了这个要求具有发布调试模块的实例应用的最大开发挑战。


// BundleConfig.cs
using System.Web;
using System.Web.Optimization;

public class BundleConfig
{
    // For more information on bundling, visit http://go.microsft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
            "~/Scripts/jquery-{version}.js"));

        bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
            "~/Scripts/bootstrap.js",
            "~/Scripts/respond.js"
        ));

        bundles.Add(new StyleBundle("~/Content/css").Include(
           "~/Content/bootstrap.css",
           "~/Content/site.css",
           "~/Content/SortableGrid.css",
           "~/Content/angular-block-ui.min.css",
           "~/Content/font-awesome.min.css"
        ));

        bundles.Add(new ScriptBundle("~/bundles/angular").Include(
           "~/Scripts/angular.min.js",
           "~/Scripts/angular-route.min.js",
           "~/Scripts/angular-sanitize.min.js",
           "~/Scripts/angular-ui.min.js",
           "~/Scripts/angular-ui/ui-bootstrap.min.js",
           "~/Scripts/angular-ui/ui-bootstrap-tpls.min.js",
           "~/Scripts/angular-ui.min.js",
           "~/Scripts/angular-block-ui.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/shared").Include(
           "~/Views/Shared/CodeProjectBootstrap.js",
           "~/Views/Shared/AjaxService.js",
           "~/Views/Shared/AlertService.js",
           "~/Views/Shared/DataGridService.js",
           "~/Views/Shared/MasterController.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/routing-debug").Include(
           "~/Views/Shared/CodeProjectRouting-debug.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/routing-production").Include(
           "~/Views/Shared/CodeProjectRouting-production.js"
        ));

        bundles.Add(new ScriptBundle("~/bundles/home").Include(
           "~/Views/Home/IndexController.js",
           "~/Views/Home/AboutController.js",
           "~/Views/Home/ContactController.js",
           "~/Views/Home/InitializeDataController.js"
        ));

 
        bundles.Add(new ScriptBundle("~/bundles/customers").Include(
           "~/Views/Customers/CustomerMaintenanceController.js",
           "~/Views/Customers/CustomerInquiryController.js"
        ));

 
        bundles.Add(new ScriptBundle("~/bundles/products").Include(
           "~/Views/Products/ProductMaintenanceController.js",
           "~/Views/Products/ProductInquiryController.js"
        ));
    }
}

缓存与 ASP.NET 捆绑
使用 ASP.NET 捆绑的优势是它的“cache busting”的辅助方法,一旦你改变了 CSS 和 JavaScript 的缓存方式,这种方法将会使用自动引导的方式使捆绑的文件能够更容易的进行缓存。下面的代码示例是在一个 MVC 的 Razor 视图中执行的(通常情况下,是在 _Layout.cshtml 母版页)。所述的 Scripts.Render 方法将会在客户端渲染,并且当在非调试模式下执行时,它将会产生包的虚拟路径和结束包的序列号。当你更改包的内容并重新发布你的应用程序时,包将会生成一个新的版本号,这有助于客户端上的浏览器缓存,并生成一个新的下载包。

// _Layout.cshtml
@Scripts.Render("~/bundles/customers")
@Scripts.Render("~/bundles/products")
该 Scripts.Render 功能是一个很好的功能,但在此示例应用程序,我想使用在客户端一侧动态加载的客户和产品,所以我不能用渲染功能来渲染我的一些包,这是挑战的开始。这个问题是以如何使用 AngularJS 从客户端 JavaScript 渲染服务器端的 ASP.NET 包开始的?

_Layout.cshtml - 服务器端启动代码
一个使用 ASP.NET MVC 来引导 AngularJS 应用程序的好处是,你可以通过 _Layout.cshtml 主页中服务器端的代码,来加载和执行 AngularJS 的代码。这是第一步,帮助解决我通过客户端代码渲染服务器端捆绑的窘境。当然,你可以简单地嵌入脚本来标记客户端的代码,但我需要一种方法来渲染一个包和引用,并维护被追加到清除了缓存的包的目的自动版本号。

开始的时候,我在 _Layout.cshtml 母版页的顶部编写了一些服务器端代码。我所做的头两件事情就是让从程序集信息类中获取应用的序列号,从应用程序设置中获取检索的基本 URL。这两个都将被之后 HTML 中的 Razor 视图引擎所解析。

下面的代码段,产生了我想根据需求动态加载的一些包,我不想当应用启动时加载所有的前期的包。我需要的信息中的最重要一块是虚拟路径和每一次捆绑的长版本号。幸运的是,访问捆绑信息的方法,本身就是一种捆绑的功能。

下面的代码行的关键行引用了 BundleTable。这行代码执行了 ResolveBundleUrl, 返回了该方法的虚拟路径以及每个引用的捆绑和版本号。这些代码基本上生成一个包的列表并且将该列表转换成一个 JSON 集合。后来这个 JSON 集被添加到 AngularJS。有一个 JSON 集合中的包的信息是,允许从客户端 AngularJS 应用程序加载服务器端捆绑的最初的方法。


// _Layout.cshtml
@using CodeProject.Portal.Models
@{
    string version = typeof(CodeProject.Portal.MvcApplication).Assembly.GetName().Version.ToString();
    string baseUrl = System.Configuration.ConfigurationManager.AppSettings["BaseUrl"].ToString();

    List bundles = new List();
    CodeProject.Portal.Models.CustomBundle customBundle;

    List codeProjectBundles = new List();
    codeProjectBundles.Add("home");
    codeProjectBundles.Add("customers");
    codeProjectBundles.Add("products");

    foreach (string controller in codeProjectBundles)
    {
        customBundle = new CodeProject.Portal.Models.CustomBundle();
        customBundle.BundleName = controller;
        customBundle.Path = BundleTable.Bundles.ResolveBundleUrl("~/bundles/" + controller);
        customBundle.IsLoaded = false;
        bundles.Add(customBundle);
    }

    BundleInformation bundleInformation = new BundleInformation();
    bundleInformation.Bundles = bundles;
    string bundleInformationJSON = Newtonsoft.Json.JsonConvert.SerializeObject(
    bundleInformation, Newtonsoft.Json.Formatting.None);

}

ASP.NET 的捆绑类有很多的功能。例如,如果你想通过捆绑所有文件进行迭代,你可以执行 EnumerateFiles 方法,返回一个特定的包内的每个文件的虚拟路径。

foreach (var file in bundle.EnumerateFiles(new BundleContext(
         new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, "~/bundles/shared")))
{
    string filePath = file.IncludedVirtualPath.ToString();
}
_Layout.cshtml - 标题
在 HTML 文档的标题部分,有一个 RequireJS 的参考。该应用程序通过客户端 AngularJS 代码使用了 RequireJS 动态的加载包。RequireJS 是一个加载了 JavaScript API 模块的异步模块定义(AMD)。RequireJS 有许多功能,但是对于实例应用的目的,仅需要来自于 RequireJS 的请求功能以便在后面应用程序的使用。

此外,Scripts.Render 和 Styles.Render 方法将在开始部分被执行。当应用程序以调试模式执行或者 EnableOptimizations 被指为 false 时,渲染的方法将会在每一次捆绑中生成多个脚本。当在发布模式和启用优化时,渲染方法将生成一个脚本标记来代表整个捆绑的版本戳。

这就导致了另外一个挑战,那就是应用需要支持发布模式下生成捆绑脚本标签的能力,和调试模式下生成独特文件的脚本标签的能力。如果你想要在调试模式下为 JavaScript 代码设置断点,这点是很重要的。因为如果在发布模式下,使用 JavaScript 代码的优化捆绑版本是不可能的。

最后,在标题部分,使用 Razor 语法的基本 URL 被早早地设定为服务器侧的基本 URL 变量。


!DOCTYPE html>





AngularJS MVC Code Project</titlev></p> <p><script src="~/Scripts/require.js"></script></p> <p>@Scripts.Render("~/bundles/jquery")<br /> @Scripts.Render("~/bundles/bootstrap")<br /> @Scripts.Render("~/bundles/modernizr")<br /> @Scripts.Render("~/bundles/angular")</p> <p>@Styles.Render("~/Content/css")</p> <p><base href="#baseUrl" /></p> <p></head></p> <p>调试模式VS发布模式<br /> 当 EnableOptimizations 被设置为 false,或者在调试模式运行时,该 @Scripts.Render 方法会在每一次捆绑中产生多种脚本标签。如果你想设置断点并调试 JavaScript 文件,这是必要的。你有另一种选择,就是在调试模式下,使用 RenderFormat 方法来选人客户脚本标签。</p> <p>下面的代码片段包含在 _layout.cshtml 母版页中,当应用程序在调试模式下,RenderFormat 会被使用。在这种模式下,应用的版本序列号会被追加到捆绑中的所有JavaScript 文件的脚本标签中。对于标准的渲染脚本标签格式不包含追加版本号来说,这也算是个小弥补。</p> <p>从 Visual Studio 中启动应用程序时,您可能会遇到浏览器缓存的问题。同时也可能会花时间来猜测,你运行的是否是最新版本的 JavaScript 文件。在浏览器中按 F5 可以解决这个问题。为了避免这个问题一起发生,应用程序版本号会被附加到脚本标签中。使用自动版本插件,版本号会在每次构建中自动递增。使用这项技术,我能够知道每一次的编译和运行使用的是 JavaScript 文件的最新版本,这为我省了很多时间。</p> <p><br /> // _Layout.cshtml<br /> @if (HttpContext.Current.IsDebuggingEnabled)<br /> {<br />     @Scripts.RenderFormat("<script type="text/<a href="/js_a/js.html" target="_blank">javascript</a>" src="{0}?ver =" + @version + " "><br />                            </script>", "~/bundles/shared")<br />     @Scripts.RenderFormat("<script type="text/javascript" src="{0}?ver =" + @version + " "><br />                            </script>","~/bundles/routing-debug")<br /> }<br /> else<br /> {<br />     @Scripts.Render("~/bundles/shared")<br />     @Scripts.Render("~/bundles/routing-production")<br /> }</p> <p>服务器端 Razor 数据和 AngularJS 之间的桥梁<br /> 现在,我已经创建了服务器端的捆绑数据的收集,接下来的挑战就是注入并创建服务器端和客户端 AngularJS 代码的桥梁。在 _Layout.cshtml 母版页,我创建了能够创造一个 AngularJS 供应商的匿名的 JavaScript 功能。最初我计划创建一个常规的 AngularJS 服务或者一个包含在 _Layout.cshtml 文件中能够使用 Razor 语法注入服务器端的方法集。</p> <p>不幸的是,直到 AngularJS 配置阶段完成之后,才能提供 AngularJS 服务和方法集,因此我无法在主页中创建一个没有 AngularJS 错误的服务。为了克服这个限制,则需要创建一个 AngularJS 的提供者。提供者的功能是,能够创建提供方法集和服务的实例。提供者允许你在 Angular 配置过程中创建和配置一个服务。</p> <p>服务提供者名称是以他们所提供工作的提供商为开始的。下面的代码片段中,代码创建一个“applicationConfiguration”提供商,这个提供商正在被 applicationConfigurationProvider 引用。这个提供商将会在构造函数中被配置,来设定用于动态请求的应用所需的程序集版本号和捆绑列表。MVC Razor 代码在构造函数中会注入服务器端的数据。</p> <p><br /> // _Layout.cshtml<br /> (function () {<br />         var codeProjectApplication = angular.module('codeProject');<br />         codeProjectApplication.provider('applicationConfiguration', function () {<br />             var _version;<br />             var _bundles;<br />             return {<br />                 setVersion: function (version) {<br />                 _version = version;<br />             },</p> <p>            setBundles: function (bundles) {<br />                 _bundles = bundles;<br />             },</p> <p>            getVersion: function () {<br />                 return _version;<br />             },</p> <p>            getBundles: function () {<br />                 return _bundles;<br />             },</p> <p>            $get: function () {<br />                 return {<br />                     version: _version,<br />                     bundles: _bundles<br />                 }<br />             }<br />        }<br />     });</p> <p>    codeProjectApplication.config(function (applicationConfigurationProvider) {<br />         applicationConfigurationProvider.setVersion('@version');<br />         applicationConfigurationProvider.setBundles('@Html.Raw(bundleInformationJSON)');<br />     });<br /> })();</p> <p>路由产生和动态加载 MVC 捆绑<br /> 现在你可能已经看到了很多例子实现了每个内容页硬编码路径的 AngularJS 示例。示例应用程序的路由使用基于约定的方法,这种方法允许路由表使用硬编码的路由方法来实现使用基于约定的方法。所有的内容页和相关联的 JavaScript 文件将会遵循命名约定规则,这个规则允许该应用程序来解析路由并动态地确定每个内容页需要哪些 JavaScript 文件。</p> <p>下面的示例应用程序的路由表只需要分析出三条路线:</p> <p>一个用于根路径'/'<br /> 一个标准路由路径,如'/:section/:tree'<br /> 包含路由参数的路由,如'/:section/:tree/:id' <br /> 我决定从 ASP.NET 捆绑中加载 JavaScript 文件,下面的路由配置代码需要包含一些 applicationConfigurationProvider 引用的代码,来用于创建保存之前的捆绑信息。捆绑信息将会被解析为 JSON 集。捆绑信息集将会用于返回虚拟的捆绑路径。此外,JSON 集将被用于跟踪被加载的捆绑。一旦捆绑被加载,就不需要第二次捆绑了。</p> <p>有几件事情需要写入路由代码中。首先,每当用户选择一个页面来加载一定功能模块时,对于模块绑定的所有 JavaScript 文件需要被下载。例如,当用户选择客户模式中的一个内容页面时,以下的代码会查看模块的捆绑是否已经通过 JSON _bundles collection 的 isLoaded 属性被检查了,并且如果 isLoaded 为 false,则捆绑将会被记载, isLoaded 属性会被设置为 true。</p> <p>当确定需要下载哪些模式的捆绑时,有两件事情需要去加载捆绑:deferred promise 和 RequireJS。deferred promise 可以帮助你异步运行函数,当它完成执行,就会返回。</p> <p>现在,最后一块本文之谜是确定从客户端代码包中加载的方式。我在以前的文章 CodeProject.com 使用 RequireJS(前面提到的)来动态加载 JavaScript 文件,我使用捆绑来加载 RequireJS。使用 RequireJS“需求”的功能, 我通过捆绑的虚拟路径进入需求功能。事实证明,需求功能将会加载任何能够更好执行捆绑加载的路径。</p> <p>当我第一次使用 RequireJS 的路径来下载捆绑时,我已经完成了 RequireJS 和它的所有配置。事实证明,我能够去掉这一切,只是简单地加载 RequireJS 库并使用它的需求功能。我甚至没有使用 RequireJS 定义表述来预安装我的动态加载控制器。很多试验和错误之后,我已经达到了本文的目的。我现在可以通过客户端代码加载服务器端的捆绑。</p> <p><br /> // CodeProjectRouting-production.js<br /> ​angular.module("codeProject").config(<br /> ['$routeProvider', '$locationProvider', 'applicationConfigurationProvider'<br />     function ($routeProvider, $locationProvider, applicationConfigurationProvider) {<br />         var baseSiteUrlPath = $("base").first().attr("href");<br />         var _bundles = JSON.parse(applicationConfigurationProvider.getBundles());<br />         this.getApplicationVersion = function () {<br />             var applicationVersion = applicationConfigurationProvider.getVersion();<br />             return applicationVersion;<br />         }<br />         this.getBundle = function (bundleName) {</p> <p>            for (var i = 0; i < _bundles.Bundles.length; i++) {<br />                 if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {<br />                     return _bundles.Bundles[i].Path;<br />                 }<br />             }<br />         }<br />         this.isLoaded = function (bundleName) {<br />             for (var i = 0; i < _bundles.Bundles.length; i++) {<br />                 if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {<br />                     return _bundles.Bundles[i].IsLoaded;<br />                 }<br />             }<br />         }<br />         this.setIsLoaded = function (bundleName) {<br />             for (var i = 0; i < _bundles.length; i++) {<br />                 if (bundleName.toLowerCase() == _bundles.Bundles[i].BundleName) {<br />                     _bundles.Bundles[i].IsLoaded = true;<br />                     break;<br />                 }<br />             }<br />         }<br />         $routeProvider.when('/:section/:tree',<br />         {<br />             templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + <br />                          rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },<br />             resolve: {<br />                 load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {<br />                     var path = $location.path().split("/");<br />                     var parentPath = path[1];<br />                     var bundle = this.getBundle(parentPath);<br />                     var isBundleLoaded = this.isLoaded(parentPath);<br />                     if (isBundleLoaded == false) {<br />                         this.setIsLoaded(parentPath);<br />                         var deferred = $q.defer();<br />                         require([bundle], function () {<br />                             $rootScope.$apply(function () {<br />                                 deferred.resolve();<br />                             });<br />                         });<br />                         return deferred.promise;<br />                     }<br />                 }]<br />             }<br />         });<br />         $routeProvider.when('/:section/:tree/:id',<br />         {<br />             templateUrl: function (rp) { return baseSiteUrlPath + 'views/' + <br />                          rp.section + '/' + rp.tree + '.html?v=' + this.getApplicationVersion(); },<br />             resolve: {<br />                 load: ['$q', '$rootScope', '$location', function ($q, $rootScope, $location) {<br />                     var path = $location.path().split("/");<br />                     var parentPath = path[1];<br />                     var bundle = this.getBundle(parentPath);<br />                     var isBundleLoaded = this.isLoaded(parentPath);<br />                     if (isBundleLoaded == false) {<br />                         this.setIsLoaded(parentPath);<br />                         var deferred = $q.defer();<br />                         require([bundle], function () {<br />                             $rootScope.$apply(function () {<br />                                 deferred.resolve();<br />                             });<br />                         });<br />                         return deferred.promise;<br />                     }<br />                 }]<br />             }<br />         });<br />         $routeProvider.when('/',<br />         {<br />             templateUrl: function (rp) { <br />                 return baseSiteUrlPath + 'views/Home/Index.html?v=' + this.getApplicationVersion(); },<br />                 resolve: {<br />                 load: ['$q', '$r </div> </div> </section> <section class="wrap-box"> <div class="g-tit"> <h2>相关文章</h2> </div> <ul class="s-list nobord notop"> <li> <a href="/art-424471.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》红尘无眼完成图文攻略</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7c1e10901.jpg" alt="《燕云十六声》红尘无眼完成图文攻略" /> </div> </a> </li> <li> <a href="/art-424470.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》阴阳如影完成图文攻略</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7c03d3a21.jpg" alt="《燕云十六声》阴阳如影完成图文攻略" /> </div> </a> </li> <li> <a href="/art-424469.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》悬檐之下四架椽屋图文攻略</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7be0d6a71.jpg" alt="《燕云十六声》悬檐之下四架椽屋图文攻略" /> </div> </a> </li> <li> <a href="/art-424468.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》2024最新公测时间介绍</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7bc4d25e1.jpg" alt="《燕云十六声》2024最新公测时间介绍" /> </div> </a> </li> <li> <a href="/art-424467.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》有没有藏宝阁</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7bb77aa21.jpg" alt="《燕云十六声》有没有藏宝阁" /> </div> </a> </li> <li> <a href="/art-424466.htm" class="s-card"> <div class="s-card-l"> <p class="tit">《燕云十六声》制作公司介绍</p> <div class="info"> <span class="person">游戏攻略</span> <span class="time">2024-12-25</span> </div> </div> <div class="s-card-pic"> <img src="/images/lazy.gif" data-src="/uploads/20241225/logo_676bd7ba543751.jpg" alt="《燕云十六声》制作公司介绍" /> </div> </a> </li> </ul> </section> <section class="wrap-box"> <div class="g-tit"> <h2>精彩推荐</h2> </div> <ul class="card-box"> <li class="card3"> <a href="/app/101287.htm" target="_self" class="figure"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5caf46aa71.png" alt="闪电火柴人 官方正版v1.77.3" /> </div> <p class="figure-head">闪电火柴人 官方正版v1.77.3</p> <span class="figure-btn">下载</span> </a> </li> <li class="card3"> <a href="/app/101255.htm" target="_self" class="figure"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c57bfe211.png" alt="bvn全明星大乱斗 安卓版v3.0" /> </div> <p class="figure-head">bvn全明星大乱斗 安卓版v3.0</p> <span class="figure-btn">下载</span> </a> </li> <li class="card3"> <a href="/app/101237.htm" target="_self" class="figure"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c20e77591.png" alt="死神vs火影怜改 安卓版v怜改" /> </div> <p class="figure-head">死神vs火影怜改 安卓版v怜改</p> <span class="figure-btn">下载</span> </a> </li> <li class="card3"> <a href="/app/101233.htm" target="_self" class="figure"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c103546c1.png" alt="古荒遗迹2手游 最新版v1.0.2" /> </div> <p class="figure-head">古荒遗迹2手游 最新版v1.0.2</p> <span class="figure-btn">下载</span> </a> </li> </ul> <ul class="card-box-b"> <li class="card10"> <a href="/app/101304.htm" target="_self" class="figure2"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5cda97e8f1.png" alt="我最强舞者 (I, Best Dancer)安卓版v8" /> </div> <div class="figure-cont"> <p class="figure-head">我最强舞者 (I, Best Dancer)安卓版v8</p> <div class="figure-desc"> <span>模拟经营</span> <span>我最强舞者 (I, Best Dancer)安卓版v8</span> </div> <div class="figure-desc"> <p>我最强舞者(I, Best Dancer)是一款休闲放置类手</p> </div> </div> <span class="figure-btn">下载</span> </a> </li> <li class="card10"> <a href="/app/101280.htm" target="_self" class="figure2"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c9c319411.jpg" alt="迷你世界国服版本2024 v1.43.0" /> </div> <div class="figure-cont"> <p class="figure-head">迷你世界国服版本2024 v1.43.0</p> <div class="figure-desc"> <span>模拟经营</span> <span>迷你世界国服版本2024 v1.43.0</span> </div> <div class="figure-desc"> <p>迷你世界国服版本2024是一款自由度非常高的沙盒游戏,玩法和</p> </div> </div> <span class="figure-btn">下载</span> </a> </li> <li class="card10"> <a href="/app/101270.htm" target="_self" class="figure2"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c7ed7c861.png" alt="烹饪乐园 安卓版v1.23.6" /> </div> <div class="figure-cont"> <p class="figure-head">烹饪乐园 安卓版v1.23.6</p> <div class="figure-desc"> <span>模拟经营</span> <span>烹饪乐园 安卓版v1.23.6</span> </div> <div class="figure-desc"> <p>烹饪乐园(Cooking Town)是一款非常好玩的餐厅模拟</p> </div> </div> <span class="figure-btn">下载</span> </a> </li> <li class="card10"> <a href="/app/101265.htm" target="_self" class="figure2"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c6c949551.png" alt="迷你世界小米服 最新安卓版v1.43.0" /> </div> <div class="figure-cont"> <p class="figure-head">迷你世界小米服 最新安卓版v1.43.0</p> <div class="figure-desc"> <span>模拟经营</span> <span>迷你世界小米服 最新安卓版v1.43.0</span> </div> <div class="figure-desc"> <p>迷你世界小米版是由迷你玩科技开发的休闲模拟经营类游戏。此版本</p> </div> </div> <span class="figure-btn">下载</span> </a> </li> <li class="card10"> <a href="/app/101263.htm" target="_self" class="figure2"> <div class="figure-box"> <img src="/images/lazy.gif" data-src="https://img.111cn.net/uploads/20241225/logo_676b5c695bb271.png" alt="布娃娃Sprunki沙盒 安卓版v0.0.1" /> </div> <div class="figure-cont"> <p class="figure-head">布娃娃Sprunki沙盒 安卓版v0.0.1</p> <div class="figure-desc"> <span>模拟经营</span> <span>布娃娃Sprunki沙盒 安卓版v0.0.1</span> </div> <div class="figure-desc"> <p>布娃娃Sprunki沙盒是一个非常有趣的沙盒游戏,复古简约的</p> </div> </div> <span class="figure-btn">下载</span> </a> </li> </ul> </section> <footer class="foot"> <a href="/" class="logo-icon"> <img src="/mobile/images/logo2.png" alt="一聚教程网"> </a> <p>Copyright © 2010-2022</p> <p>111cn.net All Rights Reserved</p> </footer> <script> var advData = {"img_fixed_pc_adv":"https:\/\/img.111cn.net\/uploads\/20240509\/663c2e9729f58.jpg","img_fixed_mob_adv":"https:\/\/img.111cn.net\/uploads\/20240509\/663c2e8793225.jpg","url_adv":"http:\/\/shop.hushen.cn\/shop\/c\/baojianpin.html","str_adv":"\u864e\u795e\u5546\u57ce\uff1a\u5173\u7231\u7537\u6027\uff0c\u66f4\u61c2\u7537\u4eba\u3002\u89e3\u51b3\u5927\u4f17\u7684\u7537\u8a00\u4e4b\u9690","img_popup_adv":"https:\/\/img.111cn.net\/uploads\/20240509\/663c2e748238d.png","pc_show_img":"2","pc_show_popup":"2","pc_show_video":"2","mob_show_img":"2","mob_show_popup":"2","mob_show_video":"2","close_adv":"https:\/\/img.111cn.net\/uploads\/20240508\/663b20650801e.png","video_adv":"\/pc\/images\/pc-adv.mp4"}; </script> <script src="/jspc/funcmob.js" type="text/javascript"></script> <!-- Google tag (gtag.js) --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-DSRRGRV1TL"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-DSRRGRV1TL'); </script> <div class="back-top" style="display: block;"> <span class="icon-box"> <svg class="icon" viewBox="0 0 1024 1024"> <path d="M213.333333 640h170.666667v256h256v-256h170.666667l-298.666667-341.333333zM170.666667 128h682.666666v85.333333H170.666667z" fill="#0374f3"></path> </svg> </span> </div> </div> <script src="/js/stat.js"></script> </body> </html>