AngularJs篇:使用AngularJs打造一个简易权限系统的实现代码

一、引言

上一篇博文已经向大家介绍了AngularJS核心的一些知识点,在这篇博文将介绍如何把AngularJs应用到实际项目中。本篇博文将使用AngularJS来打造一个简易的权限管理系统。下面不多说,直接进入主题。

二、整体架构设计介绍

首先看下整个项目的架构设计图:

从上图可以看出整个项目的一个整体结构,接下来,我来详细介绍了项目的整体架构:

采用Asp.net Web API来实现REST 服务。这样的实现方式,已达到后端服务的公用、分别部署和更好地扩展。Web层依赖应用服务接口,并且使用Castle Windsor实现依赖注入。

1、显示层(用户UI)

显示层采用了AngularJS来实现的SPA页面。所有的页面数据都是异步加载和局部刷新,这样的实现将会有更好的用户体验。

2、应用层(Application Service)

AngularJS通过Http服务去请求Web API来获得数据,而Web API的实现则是调用应用层来请求数据。

3、基础架构层

基础架构层包括仓储的实现和一些公用方法的实现。

仓储层的实现采用EF Code First的方式来实现的,并使用EF Migration的方式来创建数据库和更新数据库。

LH.Common层实现了一些公用的方法,如日志帮助类、表达式树扩展等类的实现。

4、领域层

领域层主要实现了该项目的所有领域模型,其中包括领域模型的实现和仓储接口的定义。

介绍完整体结构外,接下来将分别介绍该项目的后端服务实现和Web前端的实现。

三、后端服务实现

后端服务主要采用Asp.net Web API来实现后端服务,并且采用Castle Windsor来完成依赖注入。

这里拿权限管理中的用户管理来介绍Rest Web API服务的实现。

提供用户数据的REST服务的实现:

public class UserController : ApiController
  {
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
      _userService = userService;
    }

    [HttpGet]
    [Route("api/user/GetUsers")]
    public OutputBase GetUsers([FromUri]PageInput input)
    {
      return _userService.GetUsers(input);
    }

    [HttpGet]
    [Route("api/user/UserInfo")]
    public OutputBase GetUserInfo(int id)
    {
      return _userService.GetUser(id);
    }

    [HttpPost]
    [Route("api/user/AddUser")]
    public OutputBase CreateUser([FromBody] UserDto userDto)
    {
      return _userService.AddUser(userDto);
    }

    [HttpPost]
    [Route("api/user/UpdateUser")]
    public OutputBase UpdateUser([FromBody] UserDto userDto)
    {
      return _userService.UpdateUser(userDto);
    }

    [HttpPost]
    [Route("api/user/UpdateRoles")]
    public OutputBase UpdateRoles([FromBody] UserDto userDto)
    {
      return _userService.UpdateRoles(userDto);
    }

    [HttpPost]
    [Route("api/user/DeleteUser/{id}")]
    public OutputBase DeleteUser(int id)
    {
      return _userService.DeleteUser(id);
    }

    [HttpPost]
    [Route("api/user/DeleteRole/{id}/{roleId}")]
    public OutputBase DeleteRole(int id, int roleId)
    {
      return _userService.DeleteRole(id, roleId);
    }
  }

从上面代码实现可以看出,User REST 服务依赖与IUserService接口,并且也没有像传统的方式将所有的业务逻辑放在Web API实现中,而是将具体的一些业务实现封装到对应的应用层中,Rest API只负责调用对应的应用层中的服务。这样设计好处有:

1.REST 服务部依赖与应用层接口,使得职责分离,将应用层服务的实例化交给单独的依赖注入容器去完成,而REST服务只负责调用对应应用服务的方法来获取数据。采用依赖接口而不依赖与具体类的实现,使得类与类之间低耦合。

2.REST服务内不包括具体的业务逻辑实现。这样的设计可以使得服务更好地分离,如果你后期想用WCF来实现REST服务的,这样就不需要重复在WCF的REST服务类中重复写一篇Web API中的逻辑了,这时候完全可以调用应用服务的接口方法来实现WCF REST服务。所以将业务逻辑实现抽到应用服务层去实现,这样的设计将使得REST 服务职责更加单一,REST服务实现更容易扩展。

用户应用服务的实现:

public class UserService : BaseService, IUserService
  {
    private readonly IUserRepository _userRepository;
    private readonly IUserRoleRepository _userRoleRepository;
    public UserService(IUserRepository userRepository, IUserRoleRepository userRoleRepository)
    {
      _userRepository = userRepository;
      _userRoleRepository = userRoleRepository;
    }

    public GetResults<UserDto> GetUsers(PageInput input)
    {
      var result = GetDefault<GetResults<UserDto>>();
      var filterExp = BuildExpression(input);
      var query = _userRepository.Find(filterExp, user => user.Id, SortOrder.Descending, input.Current, input.Size);
      result.Total = _userRepository.Find(filterExp).Count();
      result.Data = query.Select(user => new UserDto()
      {
        Id = user.Id,
        CreateTime = user.CreationTime,
        Email = user.Email,
        State = user.State,
        Name = user.Name,
        RealName = user.RealName,
        Password = "*******",
        Roles = user.UserRoles.Take(4).Select(z => new BaseEntityDto()
        {
          Id = z.Role.Id,
          Name = z.Role.RoleName
        }).ToList(),

        TotalRole = user.UserRoles.Count()
      }).ToList();

      return result;
    }

    public UpdateResult UpdateUser(UserDto user)
    {
      var result = GetDefault<UpdateResult>();
      var existUser = _userRepository.FindSingle(u => u.Id == user.Id);
      if (existUser == null)
      {
        result.Message = "USER_NOT_EXIST";
        result.StateCode = 0x00303;
        return result;
      }
      if (IsHasSameName(existUser.Name, existUser.Id))
      {
        result.Message = "USER_NAME_HAS_EXIST";
        result.StateCode = 0x00302;
        return result;
      }

      existUser.RealName = user.RealName;
      existUser.Name = user.Name;
      existUser.State = user.State;
      existUser.Email = user.Email;
      _userRepository.Update(existUser);
      _userRepository.Commit();
      result.IsSaved = true;
      return result;
    }

    public CreateResult<int> AddUser(UserDto userDto)
    {
      var result = GetDefault<CreateResult<int>>();
      if (IsHasSameName(userDto.Name, userDto.Id))
      {
        result.Message = "USER_NAME_HAS_EXIST";
        result.StateCode = 0x00302;
        return result;
      }
      var user = new User()
      {
        CreationTime = DateTime.Now,
        Password = "",
        Email = userDto.Email,
        State = userDto.State,
        RealName = userDto.RealName,
        Name = userDto.Name
      };

      _userRepository.Add(user);
      _userRepository.Commit();
      result.Id = user.Id;
      result.IsCreated = true;
      return result;
    }

    public DeleteResult DeleteUser(int userId)
    {
      var result = GetDefault<DeleteResult>();
      var user = _userRepository.FindSingle(x => x.Id == userId);
      if (user != null)
      {
        _userRepository.Delete(user);
        _userRepository.Commit();
      }
      result.IsDeleted = true;
      return result;
    }

    public UpdateResult UpdatePwd(UserDto user)
    {
      var result = GetDefault<UpdateResult>();
      var userEntity =_userRepository.FindSingle(x => x.Id == user.Id);
      if (userEntity == null)
      {
        result.Message = string.Format("当前编辑的用户“{0}”已经不存在", user.Name);
        return result;
      }
      userEntity.Password = user.Password;
      _userRepository.Commit();
      result.IsSaved = true;
      return result;
    }

    public GetResult<UserDto> GetUser(int userId)
    {
      var result = GetDefault<GetResult<UserDto>>();
      var model = _userRepository.FindSingle(x => x.Id == userId);
      if (model == null)
      {
        result.Message = "USE_NOT_EXIST";
        result.StateCode = 0x00402;
        return result;
      }
      result.Data = new UserDto()
      {
        CreateTime = model.CreationTime,
        Email = model.Email,
        Id = model.Id,
        RealName = model.RealName,
        State = model.State,
        Name = model.Name,
        Password = "*******"
      };
      return result;
    }

    public UpdateResult UpdateRoles(UserDto user)
    {
      var result = GetDefault<UpdateResult>();
      var model = _userRepository.FindSingle(x => x.Id == user.Id);
      if (model == null)
      {
        result.Message = "USE_NOT_EXIST";
        result.StateCode = 0x00402;
        return result;
      }

      var list = model.UserRoles.ToList();
      if (user.Roles != null)
      {
        foreach (var item in user.Roles)
        {
          if (!list.Exists(x => x.Role.Id == item.Id))
          {
            _userRoleRepository.Add(new UserRole { RoleId = item.Id, UserId = model.Id });
          }
        }

        foreach (var item in list)
        {
          if (!user.Roles.Exists(x => x.Id == item.Id))
          {
            _userRoleRepository.Delete(item);
          }
        }

        _userRoleRepository.Commit();
        _userRepository.Commit();
      }

      result.IsSaved = true;
      return result;
    }

    public DeleteResult DeleteRole(int userId, int roleId)
    {
      var result = GetDefault<DeleteResult>();
      var model = _userRoleRepository.FindSingle(x => x.UserId == userId && x.RoleId == roleId);
      if (model != null)
      {
        _userRoleRepository.Delete(model);
        _userRoleRepository.Commit();
      }

      result.IsDeleted = true;
      return result;
    }

    public bool Exist(string username, string password)
    {
      return _userRepository.FindSingle(u => u.Name == username && u.Password == password) != null;
    }

    private bool IsHasSameName(string name, int userId)
    {
      return !string.IsNullOrWhiteSpace(name) && _userRepository.Find(u=>u.Name ==name && u.Id != userId).Any();
    }

    private Expression<Func<User, bool>> BuildExpression(PageInput pageInput)
    {
      Expression<Func<User, bool>> filterExp = user => true;
      if (string.IsNullOrWhiteSpace(pageInput.Name))
        return filterExp;

      switch (pageInput.Type)
      {
        case 0:
          filterExp = user => user.Name.Contains(pageInput.Name) || user.Email.Contains(pageInput.Name);
          break;
        case 1:
          filterExp = user => user.Name.Contains(pageInput.Name);
          break;
        case 2:
          filterExp = user => user.Email.Contains(pageInput.Name);
          break;
      }

      return filterExp;
    }
  }

这里应用服务层其实还可以进一步的优化,实现代码层级的读写分离,定义IReadOnlyService接口和IWriteServie接口,并且把写操作可以采用泛型方法的方式抽象到BaseService中去实现。这样一些增删改操作实现公用,之所以可以将这里操作实现公用,是因为这些操作都是非常类似的,无非是操作的实体不一样罢了。

仓储层的实现:

用户应用服务也没有直接依赖与具体的仓储类,同样也是依赖其接口。对应的用户仓储类的实现如下:

public class BaseRepository<TEntity> : IRepository<TEntity>
    where TEntity :class , IEntity
  {
    private readonly ThreadLocal<UserManagerDBContext> _localCtx = new ThreadLocal<UserManagerDBContext>(() => new UserManagerDBContext());

    public UserManagerDBContext DbContext { get { return _localCtx.Value; } }

    public TEntity FindSingle(Expression<Func<TEntity, bool>> exp = null)
    {
      return DbContext.Set<TEntity>().AsNoTracking().FirstOrDefault(exp);
    }

    public IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> exp = null)
    {
      return Filter(exp);
    }

    public IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> expression, Expression<Func<TEntity, dynamic>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
    {
      if (pageNumber <= 0)
        throw new ArgumentOutOfRangeException("pageNumber", pageNumber, "pageNumber must great than or equal to 1.");
      if (pageSize <= 0)
        throw new ArgumentOutOfRangeException("pageSize", pageSize, "pageSize must great than or equal to 1.");

      var query = DbContext.Set<TEntity>().Where(expression);
      var skip = (pageNumber - 1) * pageSize;
      var take = pageSize;
      if (sortPredicate == null)
        throw new InvalidOperationException("Based on the paging query must specify sorting fields and sort order.");

      switch (sortOrder)
      {
        case SortOrder.Ascending:
          var pagedAscending = query.SortBy(sortPredicate).Skip(skip).Take(take);

          return pagedAscending;
        case SortOrder.Descending:
          var pagedDescending = query.SortByDescending(sortPredicate).Skip(skip).Take(take);
          return pagedDescending;
      }

      throw new InvalidOperationException("Based on the paging query must specify sorting fields and sort order.");
    }

    public int GetCount(Expression<Func<TEntity, bool>> exp = null)
    {
      return Filter(exp).Count();
    }

    public void Add(TEntity entity)
    {
      DbContext.Set<TEntity>().Add(entity);
    }

    public void Update(TEntity entity)
    {
      DbContext.Entry(entity).State = EntityState.Modified;
    }

    public void Delete(TEntity entity)
    {
      DbContext.Entry(entity).State = EntityState.Deleted;
      DbContext.Set<TEntity>().Remove(entity);
    }

    public void Delete(ICollection<TEntity> entityCollection)
    {
      if(entityCollection.Count ==0)
        return;

      DbContext.Set<TEntity>().Attach(entityCollection.First());
      DbContext.Set<TEntity>().RemoveRange(entityCollection);
    }

    private IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> exp)
    {
      var dbSet = DbContext.Set<TEntity>().AsQueryable();
      if (exp != null)
        dbSet = dbSet.Where(exp);
      return dbSet;
    }

    public void Commit()
    {
      DbContext.SaveChanges();
    }
  }

public class UserRepository :BaseRepository<User>, IUserRepository
  {

  }

 四、AngularJS前端实现

Web前端的实现就是采用AngularJS来实现,并且采用模块化开发模式。具体Web前端的代码结构如下图所示:

App/images // 存放Web前端使用的图片资源

App/Styles // 存放样式文件

App/scripts // 整个Web前端用到的脚本文件
        / Controllers // angularJS控制器模块存放目录
        / directives // angularJs指令模块存放目录
       /  filters // 过滤器模块存放目录
       /  services // 服务模块存放目录
      / app.js // Web前端程序配置模块(路由配置)
App/Modules // 项目依赖库,angular、Bootstrap、Jquery库

App/Views // AngularJs视图模板存放目录

使用AngularJS开发的Web应用程序的代码之间的调用层次和后端基本一致,也是视图页面——》控制器模块——》服务模块——》Web API服务。

并且Web前端CSS和JS资源的加载采用了Bundle的方式来减少请求资源的次数,从而加快页面加载时间。具体Bundle类的配置:

public class BundleConfig
  {
    // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
      //类库依赖文件
      bundles.Add(new ScriptBundle("~/js/base/lib").Include(
          "~/app/modules/jquery-1.11.2.min.js",
          "~/app/modules/angular/angular.min.js",
          "~/app/modules/angular/angular-route.min.js",
          "~/app/modules/bootstrap/js/ui-bootstrap-tpls-0.13.0.min.js",
          "~/app/modules/bootstrap-notify/bootstrap-notify.min.js"
          ));
      //angularjs 项目文件
      bundles.Add(new ScriptBundle("~/js/angularjs/app").Include(
          "~/app/scripts/services/*.js",
          "~/app/scripts/controllers/*.js",
          "~/app/scripts/directives/*.js",
          "~/app/scripts/filters/*.js",
          "~/app/scripts/app.js"));
      //样式
      bundles.Add(new StyleBundle("~/js/base/style").Include(
          "~/app/modules/bootstrap/css/bootstrap.min.css",
          "~/app/styles/dashboard.css",
          "~/app/styles/console.css"
          ));
    }
  }

首页 Index.cshtml

<!DOCTYPE html>
<html ng-app="LH">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>简易权限管理系统Demo</title>
  @Styles.Render("~/js/base/style")
  @Scripts.Render("~/js/base/lib")
</head>
<body ng-controller="navigation">
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="/">简易权限管理系统Demo</a>
      </div>
      <div class="navbar-collapse collapse">
        <ul class="nav navbar-nav navbar-left">
          <li class="{{item.isActive?'active':''}}" ng-repeat="item in ls">
            <a href="#{{item.urls[0].link}}">{{item.name}}</a>
          </li>
        </ul>
        <div class="navbar-form navbar-right">
          <a href="@Url.Action("UnLogin", "Home", null)" class="btn btn-danger">
            {{lang.exit}}
          </a>
        </div>
      </div>
    </div>
  </nav>
  <div class="container-fluid">
    <div class="row">
      <div class="col-sm-3 col-md-2 sidebar">
        <ul class="nav nav-sidebar">
          <li class="{{item.isActive?'active':''}}" ng-repeat="item in urls"><a href="#{{item.link}}">{{item.title}}</a></li>
        </ul>
      </div>
      <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
        <div ng-view></div>
      </div>
    </div>
  </div>
  @Scripts.Render("~/js/angularjs/app")
</body>
</html>

五、运行效果

介绍完前后端的实现之后,接下来让我们看下整个项目的运行效果:

六、总结

到此,本文的所有内容都介绍完了,尽管本文的AngularJS的应用项目还有很多完善的地方,例如没有缓冲的支持、没有实现读写分离,没有对一些API进行压力测试等。但AngularJS在实际项目中的应用基本是这样的,大家如果在项目中有需要用到AngularJS,正好你们公司的后台又是.NET的话,相信本文的分享可以是一个很好的参考。

本文所有源码下载地址:PrivilegeManagement

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 如何使用AngularJs打造权限管理系统【简易型】

    一.引言 本文将介绍如何把AngularJs应用到实际项目中.本篇文章将使用AngularJS来打造一个简易的权限管理系统.下面不多说,直接进入主题. 二.整体架构设计介绍 首先看下整个项目的架构设计图: 从上图可以看出整个项目的一个整体结构,接下来,我来详细介绍了项目的整体架构: 采用Asp.net Web API来实现REST 服务.这样的实现方式,已达到后端服务的公用.分别部署和更好地扩展.Web层依赖应用服务接口,并且使用Castle Windsor实现依赖注入. 显示层(用户UI) 显

  • AngularJs篇:使用AngularJs打造一个简易权限系统的实现代码

    一.引言 上一篇博文已经向大家介绍了AngularJS核心的一些知识点,在这篇博文将介绍如何把AngularJs应用到实际项目中.本篇博文将使用AngularJS来打造一个简易的权限管理系统.下面不多说,直接进入主题. 二.整体架构设计介绍 首先看下整个项目的架构设计图: 从上图可以看出整个项目的一个整体结构,接下来,我来详细介绍了项目的整体架构: 采用Asp.net Web API来实现REST 服务.这样的实现方式,已达到后端服务的公用.分别部署和更好地扩展.Web层依赖应用服务接口,并且使

  • 利用Java手写一个简易的lombok的示例代码

    目录 1.概述 2.lombok使用方法 3.lombok原理解析 4.手写简易lombok 1.概述 在面向对象编程中,必不可少的需要在代码中定义对象模型,而在基于Java的业务平台开发实践中尤其如此.相信大家在平时开发中也深有感触,本来是没有多少代码开发量的,但是因为定义的业务模型对象比较多,而需要重复写Getter/Setter.构造器方法.字符串输出的ToString方法.Equals/HashCode方法等.我们都知道Lombok能够替大家完成这些繁琐的操作,但是其背后的原理很少有人会

  • Python利用tkinter实现一个简易番茄钟的示例代码

    之前捣鼓树莓派时,要求做一个番茄钟,但最后就只是搞成一个与树莓派没啥关系的py程序,虽然简陋,但就此记录一下自学的成果. 程序实现番茄工作法:25分钟工作,5分钟休息 完成一次番茄工作时间,就记一个番茄 (不把休息时间算在里面,有时候自己都不想休息,好吧,是我不知道怎么把番茄工作时间和休息时间联系在一块来记录番茄个数) 这个程序倒计时显示的是从24:59开始,是因为按的时候算是1秒? 运行界面如下: 自己感觉这个界面还行,朴素中带着点高级感 代码参考了一些大佬写的番茄钟程序,特别是那个倒计时的实

  • Spring Boot中使用 Spring Security 构建权限系统的示例代码

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作. 权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重.在Spring Boot中使用 Spring Security 构建权限系统是非常轻松和简单的.下面我们就来快速入门 Spring Security .在开始前我们需要一对

  • 一个简易时钟效果js实现代码

    本文实例为大家分享了js时钟特效 的具体代码,供大家参考,具体内容如下 js代码 var canvas = document.getElementById("clock"); var clock = canvas.getContext("2d"); function zhong() { clock.save(); //开始画外层圆 clock.translate(200, 200); clock.strokeStyle = 'black'; clock.lineWi

  • Django的用户模块与权限系统的示例代码

    一 导言 设计一个好的用户系统往往不是那么容易,Django提供的用户系统可以快速实现基本的功能,并可以在此基础上继续扩展以满足我们的需求. 先看看Django的用户系统都提供哪些功能: 提供用户模块(User Model) 权限验证(默认添加已有模块的增加删除修改权限) 用户组与组权限功能 用户鉴权与登录功能 与用户登录验证相关的一些函数与装饰方法 如配置了Django的用户系统,仅需调用Django提供的几个函数,便可实现用户的登录,注销,权限验证等功能.例如以下情景 1.登录 # some

  • c++实现一个简易的网络缓冲区的实践

    目录 1. 前言 2. 数据结构 3. 外部接口设计与实现 4. 完整代码与测试 1. 前言 请思考以下几个问题: 1).为什么需要设计网络缓冲区,内核中不是有读写缓冲区吗? 需要设计的网络缓冲区和内核中TCP缓冲区的关系如下图所示,通过socket进行进行绑定.具体来说网络缓冲区包括读(接收)缓冲区和写(发送)缓冲区.设计读缓冲区的目的是:当从TCP中读数据时,不能确定读到的是一个完整的数据包,如果是不完整的数据包,需要先放入缓冲区中进行缓存,直到数据包完整才进行业务处理.设计写缓冲区的目的是

  • Java实现一个简易版的多级菜单功能

    一:前言 最近老师布置了给多级菜单的作业,感觉蛮有意思的,可以提升自己的逻辑!下面我写个简易版的多级菜单,本人还是菜鸟,欢迎各位给予宝贵的建议! 二:正文 由于是给各位演示,所有我把涉及的其他条件全省略了,只做了给最简单的,以便大家能更好的理解我的思路 1,首先是数据库的设计 DROP TABLE IF EXISTS `t_category`; CREATE TABLE `t_category` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标识

  • VUE 实现一个简易老虎机的项目实践

    目录 简单分析下 先看看效果 html js部分 总结 今天突然要做一个竖直滚动老虎机,可以设置中奖位置,以及中奖回调,然后再带点常规的滚动动画,还是有点意思,和之前的转盘抽奖有点类似,有兴趣可以看下. 简单分析下 UI,ui的话,就简单点,三个列表从下往上滚动,搞个框罩住 css的活,应该简单. 动画,老规矩,我们采用之前的方案,动态设置 css,可以搞定. 设置中奖位置,我们可以想传递一个数组,类似 [1,2,3] 这样,数组每一项代表停留位置,那我们就可以计算得css应该平移的距离值,至于

  • webpack5搭建一个简易的react脚手架项目实践

    目录 项目初始化 安装webpack 搭建脚手架目录结构 开启本地服务 配置css&sass 安装react的相关依赖 项目添加热更新 生产环境打包 总结 项目初始化 首先我们创建一个目录,初始化 npm,得到一个package.json文件. mkdir react-cli cd react-cli npm init -y 安装webpack 安装webpack和相关依赖.webpack-dev-server是开启开发环境的服务,webpack-merge是合并公共配置文件. npm inst

随机推荐