DelphiMVCFrameWork 源码分析(二)

4 篇文章 0 订阅

路由和控制器(Controllers and routing)

DelphiMVCFrameWork框架的路由(Router)是通过Delphi的Attribute来实现的,作用于Controler类和方法。这样的实现路由的方法比较高效简单,也可能会缺乏灵活性。但是一般项目的应用也足够了。

DelphiMVCFrameWork框架的应用程序总是包含以下部分(摘自框架文档):

1、一个 Delphi WebBroker Module

2、一个 TMVCEngine(引擎)"Hook" WebModule

3、TMVCEngine容纳一个或多个Controler(继承TMVCController)

4、每个Controler包含一个或多个动作(Action),Action就是Controler的公开方法(public Method),同时伴随着RTTI Attribute(包含Router和其它参数)。

5、(可选)一个或多个中间件(Middleware,实现了接口IMVCMiddleware)的类对象)。

一般控制器和URL对应关系:

URL:www.testserver.com/products/categories/books

Server:www.testserver.com

Controler: products(映射为Controler实例)

Action: categories(映射为Controler方法)

Action Params: books(方法参数,有几种映射方式,见后面Demo)

URL第一个斜杠(slash)后字符串映射为Controler类,第二个斜杠后字符串映射为Controler方法,第三个斜杠后(如果有)映射为方法的参数。

如果方法上有MVCPath Attribute指定了,则按照Attribute指定解析。

路由(Router)

路由是通过RTTI Attribute来实现的。

1、MVCPath

MVCPath是最主要的一个Attribute,这是一个强制性的Attribute。MVCPath作用于Controler和Controler Method。可以通过向导生成Controler,也可以自己写:

 type
   [MVCPath('/products')] //MVCPath 作用于controller
   TProductsController = class(TMVCController)
     public
     [MVCPath()] //MVCPath 作用于方法(Action),如果为空,也可以省略
     procedure GetProducts; //此方法(action) 相当于 /products
     [MVCPath('/($productid)')]
     procedure GetProductByID; //此方法(action) 相当于 /products/123
   end;

2、动作参数(Action Params)处理

有4种处理参数的方法:

  • 参数包含在URL中,也就是URL映射参数
  • 字符串查询参数
  • HTML表单值(Form Values)
  • Cookies参数

参数获取格式:Context.Request.Params['ParamName']

参数获取顺序是:

  •  URL映射参数
  • 字符串查询参数
  • 表单参数(FORM Params)
  • Cookie Field

URL映射参数:

比如:GET    /orders/abc/2021/10

[MVCPath('/orders')]
TOrderControler = class(TMVCControler)

public
  [MVCPath('/($company')/($year)/($month)']
  [MVCHTTPMethod([httpGET])]
  procedure GetOrders;
end;

TOrderControler.GetOrders;
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................

  LCompany := Context.Request.Params['company'];  // '$company': abc
  LYear := Context.Request.Params['year'].ToInteger; 
  LMonth := Context.Request.ParamsAsInteger['month']; 

...................
end;

字符串查询参数:

字符串查询参数是通过"?"来表示的,是一种常见的形式。

比如:GET    /orders/abc?year=2021?month=10

[MVCPath('/orders/($company)')]
[MVCHTTPMethod([httpGET])]
procedure GetOrders;

............


TOrderControler.GetOrders;
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................
  LCompany := Context.Request.Params['company'];  // '$company': abc
  LYear := Context.Request.Params['year'].ToInteger; 
  LMonth := Context.Request.ParamsAsInteger['month']; 
...................
end;

注意:查询参数形式,也可以通过专门的方法来处理:

Context.Request.QueryStringParam('myparam')

Context.Request.QueryStringParamExists('myparam')

Context.Request.QueryStringParams()。

另外,还有一些注意的地方:

一个动作(方法)可以匹配多个路由。

Arrtibute 的名称和Context.Request.Params[ParamName]的“ParamName”必须匹配。

强类型动作(方法)

DelphiMVCFramework框架可以自动将从MVCPAHT属性获得的值注入。推荐这种方法。

[MVCPath('/orders')]
TOrderControler = class(TMVCControler)

public
  [MVCPath('/($company')/($year)/($month)']
  [MVCHTTPMethod([httpGET])]
  procedure GetOrders(const ACompany: string; const AYear, AMonth: Integer);
end;

TOrderControler.GetOrders(const ACompany: string; const AYear, AMonth: Integer);
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................

  LCompany := Context.Request.Params['company'];  // '$company': abc
  LYear := Context.Request.Params['year'].ToInteger; 
  LMonth := Context.Request.ParamsAsInteger['month']; 

...................
end;

强类型支持以下数据类型:

Integer 
Int64
Single 
Double 
Extended 
Boolean 
TDate 
TTime
TDateTime 
String 

MVCHTTPMethod attribute

DelphiMVCFramework框架支持的HTTP方法:

Verb(动词)       Action(动作)                                                        Idempotent(幂等)

GET               用有关资源的信息进行响应                                     yes

POST            所有提交的数据创建新的数据实例                           no

PUT              所有提交的数据创建或更新数据实例                        yes

DELETE       删除资源实例                                                            yes

HEAD          获取资源的元数据                                                      yes

MVCProduces attribute

设置HTTP Header 的content-type。如果设置了MVCProduces,如果客户端的请求 http header中没有设置相应的accep,则直接返回 404: Not Found.

MVCConsumes attribute

约定俗成的,一般只有POST和PUT能提交body数据。这个Attribute就是指明了客户端提交的body数据格式。

MVCDoc attribute

这是一个辅助的attribute,用于提供简要描述Controler或Action(方法)。

另外还有一些辅助的功能,

动作过滤器:

 procedure OnBeforeAction(AContext: TWebContext;
 const AActionName: string;
 var AHandled: Boolean); override;
 procedure OnAfterAction(AContext: TWebContext;
 const AActionName: string); override;

控制器相关:

 procedure MVCControllerAfterCreate; override;

 procedure MVCControllerBeforeDestroy; override;

以上简要描述了路由、控制器和动作,以及一些想关的功能。

看看路由和控制器源码的实现,重点是URI的参数和控制器的方法参数的对应处理。

DelphiMVCFrameWork 源码分析(一)_看那山瞧那水的博客-CSDN博客

 说过,DelphiMVCFrameWork框架通过 TWebModule的OnBeforeDispatch事件来接受后续处理的


constructor TMVCEngine.Create(const AWebModule: TWebModule; const AConfigAction: TProc<TMVCConfig>;
  const ACustomLogger: ILogWriter);
begin
  inherited Create(AWebModule);
  FWebModule := AWebModule;
  FixUpWebModule;
  FConfig := TMVCConfig.Create;

.................


procedure TMVCEngine.FixUpWebModule;
begin
  FSavedOnBeforeDispatch := FWebModule.BeforeDispatch;
  FWebModule.BeforeDispatch := OnBeforeDispatch;
end;
 

TMVCEngine的Create()方法中,传入TWebModule实例,并设置

FWebModule.BeforeDispatch := OnBeforeDispatch;

OnBeforeDispatch()代码:


procedure TMVCEngine.OnBeforeDispatch(ASender: TObject; ARequest: TWebRequest;
  AResponse: TWebResponse; var AHandled: Boolean);
begin
  AHandled := False;
  { there is a bug in WebBroker Linux on 10.2.1 tokyo }
  // if Assigned(FSavedOnBeforeDispatch) then
  // begin
  // FSavedOnBeforeDispatch(ASender, ARequest, AResponse, AHandled);
  // end;

  if IsShuttingDown then
  begin
    AResponse.StatusCode := http_status.ServiceUnavailable;
    AResponse.ContentType := TMVCMediaType.TEXT_PLAIN;
    AResponse.Content := 'Server is shutting down';
    AHandled := True;
  end;

  if not AHandled then
  begin
    try
      AHandled := ExecuteAction(ASender, ARequest, AResponse);
      if not AHandled then
      begin
        AResponse.ContentStream := nil;
      end;
    except
      on E: Exception do
      begin
        Log.ErrorFmt('[%s] %s', [E.Classname, E.Message], LOGGERPRO_TAG);

        AResponse.StatusCode := http_status.InternalServerError; // default is Internal Server Error
        if E is EMVCException then
        begin
          AResponse.StatusCode := (E as EMVCException).HTTPErrorCode;
        end;

        AResponse.Content := E.Message;
        AResponse.SendResponse;
        AHandled := True;
      end;
    end;
  end;
end;

首先判断WebServer是否下线,然后通过ExecuteAction()来执行动作:


function TMVCEngine.ExecuteAction(const ASender: TObject; const ARequest: TWebRequest;
  const AResponse: TWebResponse): Boolean;
var
  lParamsTable: TMVCRequestParamsTable;
  lContext: TWebContext;
  lRouter: TMVCRouter;
  lHandled: Boolean;
  lResponseContentMediaType: string;
  lResponseContentCharset: string;
  lRouterMethodToCallName: string;
  lRouterControllerClazzQualifiedClassName: string;
  lSelectedController: TMVCController;
  lActionFormalParams: TArray<TRttiParameter>;
  lActualParams: TArray<TValue>;
  lBodyParameter: TObject;
begin
  Result := False;

  if ARequest.ContentLength > FConfigCache_MaxRequestSize then
  begin
    raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
      'Request size exceeded the max allowed size [%d KiB] (1)',
      [(FConfigCache_MaxRequestSize div 1024)]);
  end;

{$IF Defined(BERLINORBETTER)}
  ARequest.ReadTotalContent;

  // Double check for malicious content-length header
  if ARequest.ContentLength > FConfigCache_MaxRequestSize then
  begin
    raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
      'Request size exceeded the max allowed size [%d KiB] (2)',
      [(FConfigCache_MaxRequestSize div 1024)]);
  end;
{$ENDIF}
  lParamsTable := TMVCRequestParamsTable.Create;
  try
    lContext := TWebContext.Create(ARequest, AResponse, FConfig, FSerializers);
    try
      DefineDefaultResponseHeaders(lContext);
      DoWebContextCreateEvent(lContext);
      lHandled := False;
      lRouter := TMVCRouter.Create(FConfig, gMVCGlobalActionParamsCache);
      try // finally
        lSelectedController := nil;
        try // only for lSelectedController
          try // global exception handler
            ExecuteBeforeRoutingMiddleware(lContext, lHandled);
            if not lHandled then
            begin
              if lRouter.ExecuteRouting(ARequest.PathInfo,
                lContext.Request.GetOverwrittenHTTPMethod { lContext.Request.HTTPMethod } ,
                ARequest.ContentType, ARequest.Accept, FControllers,
                FConfigCache_DefaultContentType, FConfigCache_DefaultContentCharset,
                FConfigCache_PathPrefix, lParamsTable, lResponseContentMediaType,
                lResponseContentCharset) then
              begin
                try
                  if Assigned(lRouter.ControllerCreateAction) then
                    lSelectedController := lRouter.ControllerCreateAction()
                  else
                    lSelectedController := lRouter.ControllerClazz.Create;
                except
                  on Ex: Exception do
                  begin
                    Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                      [Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'Cannot create controller'], LOGGERPRO_TAG);
                    raise EMVCException.Create(http_status.InternalServerError,
                      'Cannot create controller');
                  end;
                end;
                lRouterMethodToCallName := lRouter.MethodToCall.Name;
                lRouterControllerClazzQualifiedClassName := lRouter.ControllerClazz.QualifiedClassName;

                MVCFramework.Logger.InitThreadVars;

                lContext.fActionQualifiedName := lRouterControllerClazzQualifiedClassName + '.'+ lRouterMethodToCallName;
                lSelectedController.Engine := Self;
                lSelectedController.Context := lContext;
                lSelectedController.ApplicationSession := FApplicationSession;
                lContext.ParamsTable := lParamsTable;
                ExecuteBeforeControllerActionMiddleware(
                  lContext,
                  lRouterControllerClazzQualifiedClassName,
                  lRouterMethodToCallName,
                  lHandled);
                if lHandled then
                  Exit(True);

                lBodyParameter := nil;
                lSelectedController.MVCControllerAfterCreate;
                try
                  lHandled := False;
                  lSelectedController.ContentType := BuildContentType(lResponseContentMediaType,
                    lResponseContentCharset);
                  lActionFormalParams := lRouter.MethodToCall.GetParameters;
                  if (Length(lActionFormalParams) = 0) then
                    SetLength(lActualParams, 0)
                  else if (Length(lActionFormalParams) = 1) and
                    (SameText(lActionFormalParams[0].ParamType.QualifiedName,
                    'MVCFramework.TWebContext')) then
                  begin
                    SetLength(lActualParams, 1);
                    lActualParams[0] := lContext;
                  end
                  else
                  begin
                    FillActualParamsForAction(lSelectedController, lContext, lActionFormalParams,
                      lRouterMethodToCallName, lActualParams, lBodyParameter);
                  end;
                  lSelectedController.OnBeforeAction(lContext, lRouterMethodToCallName, lHandled);
                  if not lHandled then
                  begin
                    try
                      lRouter.MethodToCall.Invoke(lSelectedController, lActualParams);
                    finally
                      lSelectedController.OnAfterAction(lContext, lRouterMethodToCallName);
                    end;
                  end;
                finally
                  try
                    lBodyParameter.Free;
                  except
                    on E: Exception do
                    begin
                      LogE(Format('Cannot free Body object: [CLS: %s][MSG: %s]',
                        [E.Classname, E.Message]));
                    end;
                  end;
                  lSelectedController.MVCControllerBeforeDestroy;
                end;
                ExecuteAfterControllerActionMiddleware(lContext,
                  lRouterControllerClazzQualifiedClassName,
                  lRouterMethodToCallName,
                  lHandled);
                lContext.Response.ContentType := lSelectedController.ContentType;
                fOnRouterLog(lRouter, rlsRouteFound, lContext);
              end
              else // execute-routing
              begin
                if Config[TMVCConfigKey.AllowUnhandledAction] = 'false' then
                begin
                  lContext.Response.StatusCode := http_status.NotFound;
                  lContext.Response.ReasonString := 'Not Found';
                  fOnRouterLog(lRouter, rlsRouteNotFound, lContext);
                  raise EMVCException.Create(lContext.Response.ReasonString,
                    lContext.Request.HTTPMethodAsString + ' ' + lContext.Request.PathInfo, 0,
                    http_status.NotFound);
                end
                else
                begin
                  lContext.Response.FlushOnDestroy := False;
                end;
              end; // end-execute-routing
            end; // if not handled by beforerouting
          except
            on ESess: EMVCSessionExpiredException do
            begin
              if not CustomExceptionHandling(ESess, lSelectedController, lContext) then
              begin
                Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                  [ESess.Classname, ESess.Message, GetRequestShortDescription(ARequest),
                  ESess.DetailedMessage], LOGGERPRO_TAG);
                lContext.SessionStop;
                lSelectedController.ResponseStatus(ESess.HTTPErrorCode);
                lSelectedController.Render(ESess);
              end;
            end;
            on E: EMVCException do
            begin
              if not CustomExceptionHandling(E, lSelectedController, lContext) then
              begin
                Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                  [E.Classname, E.Message, GetRequestShortDescription(ARequest), E.DetailedMessage], LOGGERPRO_TAG);
                if Assigned(lSelectedController) then
                begin
                  lSelectedController.ResponseStatus(E.HTTPErrorCode);
                  lSelectedController.Render(E);
                end
                else
                begin
                  SendRawHTTPStatus(lContext, E.HTTPErrorCode,
                    Format('[%s] %s', [E.Classname, E.Message]), E.Classname);
                end;
              end;
            end;
            on EIO: EInvalidOp do
            begin
              if not CustomExceptionHandling(EIO, lSelectedController, lContext) then
              begin
                Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                  [EIO.Classname, EIO.Message, GetRequestShortDescription(ARequest), 'Invalid Op'], LOGGERPRO_TAG);
                if Assigned(lSelectedController) then
                begin
                  lSelectedController.ResponseStatus(http_status.InternalServerError);
                  lSelectedController.Render(EIO);
                end
                else
                begin
                  SendRawHTTPStatus(lContext, http_status.InternalServerError,
                    Format('[%s] %s', [EIO.Classname, EIO.Message]), EIO.Classname);
                end;
              end;
            end;
            on Ex: Exception do
            begin
              if not CustomExceptionHandling(Ex, lSelectedController, lContext) then
              begin
                Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                  [Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'Global Action Exception Handler'], LOGGERPRO_TAG);
                if Assigned(lSelectedController) then
                begin
                  lSelectedController.ResponseStatus(http_status.InternalServerError);
                  lSelectedController.Render(Ex);
                end
                else
                begin
                  SendRawHTTPStatus(lContext, http_status.InternalServerError,
                    Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
                end;
              end;
            end;
          end;
          try
            ExecuteAfterRoutingMiddleware(lContext, lHandled);
          except
            on Ex: Exception do
            begin
              if not CustomExceptionHandling(Ex, lSelectedController, lContext) then
              begin
                Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
                  [Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'After Routing Exception Handler'], LOGGERPRO_TAG);
                if Assigned(lSelectedController) then
                begin
                  { middlewares *must* not raise unhandled exceptions }
                  lSelectedController.ResponseStatus(http_status.InternalServerError);
                  lSelectedController.Render(Ex);
                end
                else
                begin
                  SendRawHTTPStatus(lContext, http_status.InternalServerError,
                    Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
                end;
              end;
            end;
          end;
        finally
          FreeAndNil(lSelectedController);
        end;
      finally
        lRouter.Free;
      end;
    finally
      DoWebContextDestroyEvent(lContext);
      lContext.Free;
    end;
  finally
    lParamsTable.Free;
  end;
end;

分析下这个方法的主要部分:

首先判断请求内容的长度是否超长,FConfigCache_MaxRequestSize是配置常量,默认5MB(5*1024*1024, MVCFramework.Commons.pas 单元的 TMVCConstants结构)。

lParamsTable := TMVCRequestParamsTable.Create;

创建请求参数表,类型是TDictionary<string,string>(Key = Param Name; Value = Param Value)。

lContext := TWebContext.Create(ARequest, AResponse, FConfig, FSerializers);

创建WebContext。

DefineDefaultResponseHeaders(lContext);

设置默认响应头:


procedure TMVCEngine.DefineDefaultResponseHeaders(const AContext: TWebContext);
begin
  if FConfigCache_ExposeServerSignature and (not IsLibrary) then
    AContext.Response.CustomHeaders.Values['Server'] := FConfigCache_ServerSignature;
  if FConfigCache_ExposeXPoweredBy then
    AContext.Response.CustomHeaders.Values['X-Powered-By'] := 'DMVCFramework ' +
      DMVCFRAMEWORK_VERSION;
  AContext.Response.RawWebResponse.Date := Now;
end;
 

DoWebContextCreateEvent(lContext);

调用事件OnWebContextCreate()。

lRouter := TMVCRouter.Create(FConfig, gMVCGlobalActionParamsCache);

开始路由处理。

gMVCGlobalActionParamsCache是全局变量,用于全局缓存动作参数(线程安全),初始化为空:

var

gMVCGlobalActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem> = nil;

TMVCStringObjectDictionary和TMVCActionParamCacheItem:

  { This type is thread safe }
  TMVCStringObjectDictionary<T: class> = class
  private
    FMREWS: TMultiReadExclusiveWriteSynchronizer;
  protected
    fDict: TObjectDictionary<string, T>;
  public
    constructor Create; virtual;
    destructor Destroy; override;
    function TryGetValue(const Name: string; out Value: T): Boolean;
    procedure Add(const Name: string; Value: T);
  end;


  TMVCActionParamCacheItem = class
  private
    FValue: string;
    FParams: TList<string>;
  public
    constructor Create(aValue: string; aParams: TList<string>); virtual;
    destructor Destroy; override;
    function Value: string;
    function Params: TList<string>; // this should be read-only...
  end;

TMVCStringObjectDictionary是个线程安全字典列表对象。

TMVCActionParamCacheItem是动作参数缓存对象,作用见后面。

ExecuteBeforeRoutingMiddleware(lContext, lHandled);

调用中间件OnBeforeRouting()事件。

调用 lRouter.ExecuteRouting()方法:


function TMVCRouter.ExecuteRouting(const ARequestPathInfo: string;
  const ARequestMethodType: TMVCHTTPMethodType;
  const ARequestContentType, ARequestAccept: string;
  const AControllers: TObjectList<TMVCControllerDelegate>;
  const ADefaultContentType: string;
  const ADefaultContentCharset: string;
  const APathPrefix: string;
  var ARequestParams: TMVCRequestParamsTable;
  out AResponseContentMediaType: string;
  out AResponseContentCharset: string): Boolean;
var
  LRequestPathInfo: string;
  LRequestAccept: string;
  LRequestContentType: string;
  LControllerMappedPath: string;
  LControllerMappedPaths: TStringList;
  LControllerDelegate: TMVCControllerDelegate;
  LAttributes: TArray<TCustomAttribute>;
  LAtt: TCustomAttribute;
  LRttiType: TRttiType;
  LMethods: TArray<TRttiMethod>;
  LMethod: TRttiMethod;
  LMethodPath: string;
  LProduceAttribute: MVCProducesAttribute;
  lURLSegment: string;
  LItem: String;
  // JUST FOR DEBUG
  // lMethodCompatible: Boolean;
  // lContentTypeCompatible: Boolean;
  // lAcceptCompatible: Boolean;
begin
  Result := False;

  FMethodToCall := nil;
  FControllerClazz := nil;
  FControllerCreateAction := nil;

  LRequestAccept := ARequestAccept;
  LRequestContentType := ARequestContentType;
  LRequestPathInfo := ARequestPathInfo;
  if (Trim(LRequestPathInfo) = EmptyStr) then
    LRequestPathInfo := '/'
  else
  begin
    if not LRequestPathInfo.StartsWith('/') then
    begin
      LRequestPathInfo := '/' + LRequestPathInfo;
    end;
  end;
  //LRequestPathInfo := TNetEncoding.URL.EncodePath(LRequestPathInfo, [Ord('$')]);
  LRequestPathInfo := TIdURI.PathEncode(Trim(LRequestPathInfo)); //regression introduced in fix for issue 492

  TMonitor.Enter(gLock);
  try
    //LControllerMappedPaths := TArray<string>.Create();
    LControllerMappedPaths := TStringList.Create;
    try
      for LControllerDelegate in AControllers do
      begin
        LControllerMappedPaths.Clear;
        SetLength(LAttributes, 0);
        LRttiType := FRttiContext.GetType(LControllerDelegate.Clazz.ClassInfo);

        lURLSegment := LControllerDelegate.URLSegment;
        if lURLSegment.IsEmpty then
        begin
          LAttributes := LRttiType.GetAttributes;
          if (LAttributes = nil) then
            Continue;
          //LControllerMappedPaths := GetControllerMappedPath(LRttiType.Name, LAttributes);
          FillControllerMappedPaths(LRttiType.Name, LAttributes, LControllerMappedPaths);
        end
        else
        begin
          LControllerMappedPaths.Add(lURLSegment);
        end;

        for LItem in LControllerMappedPaths do
        begin
          LControllerMappedPath := LItem;
          if (LControllerMappedPath = '/') then
          begin
            LControllerMappedPath := '';
          end;

    {$IF defined(TOKYOORBETTER)}
          if not LRequestPathInfo.StartsWith(APathPrefix + LControllerMappedPath, True) then
    {$ELSE}
          if not TMVCStringHelper.StartsWith(APathPrefix + LControllerMappedPath, LRequestPathInfo, True) then
    {$ENDIF}
          begin
            Continue;
          end;
//        end;

//          if (not LControllerMappedPathFound) then
//            continue;

          LMethods := LRttiType.GetMethods; { do not use GetDeclaredMethods because JSON-RPC rely on this!! }
          for LMethod in LMethods do
          begin
            if LMethod.Visibility <> mvPublic then // 2020-08-08
              Continue;
            if (LMethod.MethodKind <> mkProcedure) { or LMethod.IsClassMethod } then
              Continue;

            LAttributes := LMethod.GetAttributes;
            if Length(LAttributes) = 0 then
              Continue;

            for LAtt in LAttributes do
            begin
              if LAtt is MVCPathAttribute then
              begin
                // THIS BLOCK IS HERE JUST FOR DEBUG
                // if LMethod.Name.Contains('GetProject') then
                // begin
                // lMethodCompatible := True; //debug here
                // end;
                // lMethodCompatible := IsHTTPMethodCompatible(ARequestMethodType, LAttributes);
                // lContentTypeCompatible := IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes);
                // lAcceptCompatible :=  IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes);

                if IsHTTPMethodCompatible(ARequestMethodType, LAttributes) and
                  IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes) and
                  IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes) then
                begin
                  LMethodPath := MVCPathAttribute(LAtt).Path;
                  if IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
                    LRequestPathInfo, ARequestParams) then
                  begin
                    FMethodToCall := LMethod;
                    FControllerClazz := LControllerDelegate.Clazz;
                    FControllerCreateAction := LControllerDelegate.CreateAction;
                    LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
                    if LProduceAttribute <> nil then
                    begin
                      AResponseContentMediaType := LProduceAttribute.Value;
                      AResponseContentCharset := LProduceAttribute.Charset;
                    end
                    else
                    begin
                      AResponseContentMediaType := ADefaultContentType;
                      AResponseContentCharset := ADefaultContentCharset;
                    end;
                    Exit(True);
                  end;
                end;
              end; // if MVCPathAttribute
            end; // for in Attributes
          end; // for in Methods
        end;
      end; // for in Controllers
    finally
      LControllerMappedPaths.Free;
    end;
  finally
    TMonitor.Exit(gLock);
  end;
end;

参数 AControllers 是在TWebModule.OnCreate事件中添加的Contoler列表。

首先检查URL的路由编码,然后线程同步处理路由信息。

首先处理TControler类本身的Attribute,获取根路由。TControler类本身必须至少有一个MVCPath Attribute,不然引发异常"Controller %s does not have MVCPath attribute"。

如果URL(LRequestPathInfo)不是以MVCPath.Path开头,则忽略,进行下一个检查。

如果URL(LRequestPathInfo)以MVCPath.Path开头,接着检查Controler方法(动作)的路由。

只对“public”和“procedure”的方法进行处理。

IsHTTPMethodCompatible():检查方法类型

IsHTTPContentTypeCompatible():检查内容类型

IsHTTPAcceptCompatible():检查客户端允许类型

IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
                    LRequestPathInfo, ARequestParams);

检查路由是否一致并还回参数列表。

APathPrefix + LControllerMappedPath + LMethodPath,是从MVCPath Attribute获取的路由,

LRequestPathInfo,是从URL获取的路由,

ARequestParams,参数列表。


function TMVCRouter.IsCompatiblePath(
  const AMVCPath: string;
  const APath: string;
  var aParams: TMVCRequestParamsTable): Boolean;

  function ToPattern(const V: string; const Names: TList<string>): string;
  var
    S: string;
  begin
    Result := V;
    for S in Names do
      Result := StringReplace(Result, '($' + S + ')', '([' + TMVCConstants.URL_MAPPED_PARAMS_ALLOWED_CHARS + ']*)',
        [rfReplaceAll]);
  end;

var
  lRegEx: TRegEx;
  lMatch: TMatch;
  lPattern: string;
  I: Integer;
  lNames: TList<string>;
  lCacheItem: TMVCActionParamCacheItem;
begin
  if not FActionParamsCache.TryGetValue(AMVCPath, lCacheItem) then
  begin
    lNames := GetParametersNames(AMVCPath);
    lPattern := ToPattern(AMVCPath, lNames);
    lCacheItem := TMVCActionParamCacheItem.Create(lPattern, lNames);
    FActionParamsCache.Add(AMVCPath, lCacheItem);
  end;

  if (APath = AMVCPath) or ((APath = '/') and (AMVCPath = '')) then
    Exit(True)
  else
  begin
    lRegEx := TRegEx.Create('^' + lCacheItem.Value + '$', [roIgnoreCase, roCompiled, roSingleLine]);
    lMatch := lRegEx.Match(APath);
    Result := lMatch.Success;
    if Result then
    begin
      for I := 1 to pred(lMatch.Groups.Count) do
      begin
        aParams.Add(lCacheItem.Params[I - 1], TIdURI.URLDecode(lMatch.Groups[I].Value));
      end;
    end;
  end;
end;

ToPattern(): 设置每个方法(动作)的正则表达式,格式为:路由 = 正则表达式,

这些正则表达式列表缓存在FActionParamsCache。

但是这个正则表达式是个常量,

TMVCConstants.URL_MAPPED_PARAMS_ALLOWED_CHARS =

' àèéùòì''"@\[\]\{\}\(\)\=;&#\.:!\_,%\w\d\x2D\x3A\$';

就是URL中允许的字符,目前这样设置,估计是为了以后的扩展处理吧。

GetParametersNames():


function TMVCRouter.GetParametersNames(const V: string): TList<string>;
var
  S: string;
  Matches: TMatchCollection;
  M: TMatch;
  I: Integer;
  lList: TList<string>;
begin
  lList := TList<string>.Create;
  try
    S := '\(\$([A-Za-z0-9\_]+)\)';
    Matches := TRegEx.Matches(V, S, [roIgnoreCase, roCompiled, roSingleLine]);
    for M in Matches do
    begin
      for I := 0 to M.Groups.Count - 1 do
      begin
        S := M.Groups[I].Value;
        if (Length(S) > 0) and (S.Chars[0] <> '(') then
        begin
          lList.Add(S);
          Break;
        end;
      end;
    end;
    Result := lList;
  except
    lList.Free;
    raise;
  end;
end;

前面说过,在方法的MVCPath Attribute 中,参数是以"($XXXX)"标识的,所以通过正则表达式可以轻松获取参数名称列表。

.................

  begin
    lRegEx := TRegEx.Create('^' + lCacheItem.Value + '$', [roIgnoreCase, roCompiled, roSingleLine]);
    lMatch := lRegEx.Match(APath);
    Result := lMatch.Success;
    if Result then
    begin
      for I := 1 to pred(lMatch.Groups.Count) do
      begin
        aParams.Add(lCacheItem.Params[I - 1], TIdURI.URLDecode(lMatch.Groups[I].Value));
      end;
    end;
  end;

.............

这部分代码从APath获取参数值列表。

总之,通过方法

IsCompatiblePath(const AMVCPath: string;  const APath: string;
  var aParams: TMVCRequestParamsTable) 

从AMVCPath获取参数名称,从APath获取参数值,然后保存在aParams中。

所以,URL的参数数量和TControler的方法的MVCPath Attribute中的参数数量要匹配,不然会异常。

                  if IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
                    LRequestPathInfo, ARequestParams) then
                  begin
                    FMethodToCall := LMethod;  //当前执行的方法(动作)
                    FControllerClazz := LControllerDelegate.Clazz;   //TControler类别
                    FControllerCreateAction := LControllerDelegate.CreateAction;  //默认=nil

                    //Reponse内容类型
                    LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
                    if LProduceAttribute <> nil then
                    begin
                      AResponseContentMediaType := LProduceAttribute.Value;
                      AResponseContentCharset := LProduceAttribute.Charset;
                    end
                    else
                    begin
                      AResponseContentMediaType := ADefaultContentType;
                      AResponseContentCharset := ADefaultContentCharset;
                    end;
                    Exit(True);
                  end;

回到TMVCEngine.ExecuteAction(),  lRouter.ExecuteRouting()成功后,

                  if Assigned(lRouter.ControllerCreateAction) then
                    lSelectedController := lRouter.ControllerCreateAction()
                  else
                    lSelectedController := lRouter.ControllerClazz.Create;

创建控制器(TControler)实例。

lRouterMethodToCallName := lRouter.MethodToCall.Name;

调用方法的名称;

lRouterControllerClazzQualifiedClassName := lRouter.ControllerClazz.QualifiedClassName;

控制器(TControler)的"合格名称",也就是全名称,包含单元名。(主要是用于注册控制器时用,是可选择项)。

MVCFramework.Logger.InitThreadVars;

默认Log初始化;

                lContext.fActionQualifiedName := lRouterControllerClazzQualifiedClassName + '.'+ lRouterMethodToCallName;
                lSelectedController.Engine := Self;
                lSelectedController.Context := lContext;
                lSelectedController.ApplicationSession := FApplicationSession;
                lContext.ParamsTable := lParamsTable;

设置相关变量;

ExecuteBeforeControllerActionMiddleware()

执行中间件的OnBeforeControllerAction()事件;

lSelectedController.MVCControllerAfterCreate;

执行控制器OnAfterCreate()事件;

                  lSelectedController.ContentType := BuildContentType(lResponseContentMediaType,
                    lResponseContentCharset);

设置Response的Content-type和字符集;

前面说过,参数传递用4种方式:

  • 参数包含在URL中,也就是URL映射参数
  • 字符串查询参数
  • HTML表单值(Form Values)
  • Cookies参数

是如何处理这几种传递参数处理的?

lActionFormalParams := lRouter.MethodToCall.GetParameters; 

获取控制器方法(动作)参数列表;

                  if (Length(lActionFormalParams) = 0) then
                    SetLength(lActualParams, 0)

对应无参数的控制器方法(动作)设计;

                  else if (Length(lActionFormalParams) = 1) and
                    (SameText(lActionFormalParams[0].ParamType.QualifiedName,
                    'MVCFramework.TWebContext')) then
                  begin
                    SetLength(lActualParams, 1);
                    lActualParams[0] := lContext;
                  end

如果只有一个参数,则必定是TWebContext类型参数;

                  else
                  begin
                    FillActualParamsForAction(lSelectedController, lContext, lActionFormalParams,
                      lRouterMethodToCallName, lActualParams, lBodyParameter);
                  end;

对应强类型参数设计的TControler。

这里比较复杂是强类型参数设计的TControler的参数处理。

FillActualParamsForAction():


procedure TMVCEngine.FillActualParamsForAction(const ASelectedController: TMVCController;
  const AContext: TWebContext; const AActionFormalParams: TArray<TRttiParameter>;
  const AActionName: string; var AActualParams: TArray<TValue>; out ABodyParameter: TObject);
var
  lParamName: string;
  I: Integer;
  lStrValue: string;
  lFromBodyAttribute: MVCFromBodyAttribute;
  lFromQueryStringAttribute: MVCFromQueryStringAttribute;
  lFromHeaderAttribute: MVCFromHeaderAttribute;
  lFromCookieAttribute: MVCFromCookieAttribute;
  lAttributeInjectedParamCount: Integer;
  lInjectedParamValue: string;
  lList: IMVCList;
  lItemClass: TClass;
begin
  ABodyParameter := nil;
  lAttributeInjectedParamCount := 0;
  SetLength(AActualParams, Length(AActionFormalParams));
  for I := 0 to Length(AActionFormalParams) - 1 do
  begin
    lParamName := AActionFormalParams[I].name;
    if Length(AActionFormalParams[I].GetAttributes) > 0 then
    begin
      // Let's check how to inject this parameter
      if TRttiUtils.HasAttribute<MVCFromBodyAttribute>(AActionFormalParams[I], lFromBodyAttribute)
      then
      begin
        Inc(lAttributeInjectedParamCount, 1);
        if AActionFormalParams[I].ParamType.QualifiedName <> 'System.string' then
        begin
          ABodyParameter := TRttiUtils.CreateObject(AActionFormalParams[I].ParamType.QualifiedName);
          if TDuckTypedList.CanBeWrappedAsList(ABodyParameter, lList) then
          begin
            lItemClass := TMVCAbstractSerializer(ASelectedController.Serializer).GetObjectTypeOfGenericList(ABodyParameter.ClassInfo);
            ASelectedController.Serializer.DeserializeCollection(ASelectedController.Context.Request.Body,
              ABodyParameter, lItemClass, stDefault, [], lFromBodyAttribute.RootNode);
          end
          else
          begin
            ASelectedController.Serializer.DeserializeObject(ASelectedController.Context.Request.Body,
              ABodyParameter, stDefault, [], lFromBodyAttribute.RootNode);
          end;
          AActualParams[I] := ABodyParameter;
        end
        else
        begin
          AActualParams[I] := ASelectedController.Context.Request.Body;
          Continue;
        end;
      end
      else if TRttiUtils.HasAttribute<MVCFromQueryStringAttribute>(AActionFormalParams[I],
        lFromQueryStringAttribute) then
      begin
        Inc(lAttributeInjectedParamCount, 1);
        lInjectedParamValue := AContext.Request.QueryStringParam
          (lFromQueryStringAttribute.ParamName);
        HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromQueryStringAttribute);
        AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
      end
      else if TRttiUtils.HasAttribute<MVCFromHeaderAttribute>(AActionFormalParams[I],
        lFromHeaderAttribute) then
      begin
        Inc(lAttributeInjectedParamCount, 1);
        lInjectedParamValue := AContext.Request.GetHeader(lFromHeaderAttribute.ParamName);
        HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromHeaderAttribute);
        AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
      end
      else if TRttiUtils.HasAttribute<MVCFromCookieAttribute>(AActionFormalParams[I],
        lFromCookieAttribute) then
      begin
        Inc(lAttributeInjectedParamCount, 1);
        lInjectedParamValue := AContext.Request.Cookie(lFromCookieAttribute.ParamName);
        HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromCookieAttribute);
        AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
      end
      else
      begin
        raise EMVCException.Create(http_status.InternalServerError,
          'Unknown custom attribute on action parameter: ' + AActionFormalParams[I].name +
          '. [HINT: Allowed attributes are MVCFromBody, MVCFromQueryString, MVCFromHeader, MVCFromCookie]');
      end;
      Continue;
    end;

    // From now on we'll check for url mapped parameters
    if not AContext.Request.SegmentParam(lParamName, lStrValue) then
      raise EMVCException.CreateFmt(http_status.BadRequest,
        'Invalid parameter %s for action %s (Hint: Here parameters names are case-sensitive)',
        [lParamName, AActionName]);
    AActualParams[I] := GetActualParam(AActionFormalParams[I], lStrValue);
  end;

  if (AContext.Request.SegmentParamsCount + lAttributeInjectedParamCount) <>
    Length(AActionFormalParams) then
    raise EMVCException.CreateFmt(http_status.BadRequest,
      'Parameters count mismatch (expected %d actual %d) for action "%s"',
      [Length(AActionFormalParams), AContext.Request.SegmentParamsCount, AActionName]);
end;

参数可以标志为3种:

FormBody、FromQueryString、FromHeader。

题外话:Delphi的Attribute可以作用于方法中的参数,比如:

type
  SameAttr = class(TCustomAttribute)

  end;


  TMyObj = class

  public
    procedure TestOne(const [SameAttr] AValue: string);
  end;

框架定义了3种Attribute:

MVCFromBodyAttribute:从Request中获取参数

MVCFromQueryStringAttribute:从查询字符串方式中获取参数(比如:GET    /orders/abc?year=2021?month=10)

MVCFromHeaderAttribute:从Reques Header中获取参数。

这个方法就不展开分析了,比较啰嗦,但是代码不难看懂。

回到主流程TMVCEngine.ExecuteAction(),

lSelectedController.OnBeforeAction(lContext, lRouterMethodToCallName, lHandled);

调用控制器的OnBeforeAction();

lRouter.MethodToCall.Invoke(lSelectedController, lActualParams);

这里是真正调用方法了。

lSelectedController.OnAfterAction(lContext, lRouterMethodToCallName);

调用控制器的OnAfterAction();

然后lSelectedController.MVCControllerBeforeDestroy;

ExecuteAfterControllerActionMiddleware();

调用中间件的OnAfterControllerAction()事件;

基本就是这样的主流程,其它异常的情况没有分析。

后面还有验证与授权,中间件,JWT以及ActivedRecord等内容。

Delphi实例开发教程》源代码包说明 __________________________________________________________________ (一)源代码程序包内容: 源代码程序包的目录结构如下: \(根目录) | |————Readme.txt(说明文件必须放在这个地方) | |————\本书大案例(目录) | | |————|————DataBase(目录,存放大案例的数据库文件为“date”和数据库连接文件TEST.UDL) | | |————|————Materials(目录,存放大案例的登录logo) | | |————|————EXE(目录,存放可执行文件,为channelplay.exe) | | |————|————Setup(目录,存放安装文件) | | |————|————Source(目录,存放源代码,这个目录可以进一步细分) | | |————|————|————code(保存源程序) | | |————|————|————dcu(保存中间编译文件) | | |————|————|————exe(保存可执行文件) 其中安装程序文件夹里面是本案例系统的安装程序,与程序源代码无关。用户既可以以它来安装信息搜索系统程序,也可以直接从setup文件夹中直接运行程序。(当然,两者前提是数据库配置好,具体的配置方法参考案例书第4章的案例分析与完善) 另外在source文件夹中还有三个文件夹,code,dcu与exe是在编写程序代码是为了方便管理而设置的(这里的exe文件夹中文件和大案例下的EXE文件夹一样)。在一般情况下,如果不对编写的delphi项目工程进行设置而直接保存到一个文件中,那么在运行程序代码是就会在此文件夹中产生中间编译文件和最后的运行程序。如此则这同一个文件夹中就不仅有项目文件、单元文件也会有在编译过程中产生的编译文件和最后程序等。这样就不方便程序的管理和查看。所以在编写delphi项目工程之前最后设置三个文件夹分别用于保存源程序代码、中间的编译文件和应用程序,以方便管理。(具体设置是先打开delphi7,选择[Project]->[options],打开一个对话框,directories/Conditionals页,在Output directory里填写保存应用程序的文件夹路径,在Unit Output directory填写保存中间编译文件的文件夹路径。这两个路径最后写与源程序代码文件夹相关的相对路径。如本书的项目工程,源程序代码保存在code文件夹中,那么Output directory里填写“..\exe”,而Unit Output directory里填写“..\dcu”。) ()下面介绍大案例code文件夹里各个文件的内容和关系:(code文件夹里的几个文件夹都与本程序无关,可以删除) 1)ChannelPlayer.dpr-------本案例的工程文件 它说明项目中各个单元文件的运行创建,并启动生成应用程序 2)MainFrm.dfm-----------------主窗体的窗体文件 它保存软件主界面窗体所作的属性 MainFrm.pas------------------主窗体的单元文件 它保存与软件主界面窗体相关的程序代码 以下各组文件的内容可以参考本书的第章的实例分析与完善 3)MaintainFrm.dfm---------------对已保存的定制进行修改维护的窗体文件 MaintainFrm.pas-------------对已保存的定制进行修改维护的单元文件 4)BrowserFrm.dfm----------浏览器的窗体文件 BrowserFrm.pas----------浏览器的单元文件 5)CustomSearchFrm.dfm-------实现定制搜索功能窗体文件 CustomSearchFrm.pas-----------实现定制搜索功能单元文件 6)DisplayFrm.dfm----------------设计显示和操作搜索信息界面的窗体文件 DisplayFrm.pas--------------设计显示和操作搜索信息界面的单元文件 7)DisplayFra.dfm------------显示的搜索到的网站的窗体文件 DisplayFra.pas------------显示的搜索到的网站的单元文件 8)InputFra.dfm--------------进行搜索前对一些基本输入控件处理的窗体文件 InputFra.pas---------------进行搜索前对一些基本输入控件处理的单元文件 9)RollingNewsFrm.dfm---------用于滚动新闻设置的窗体文件 RollingNewsFrm.pas---------用于滚动新闻设置的单元文件 10)SettingFrm.dfm-----------用于系统的设置的窗体文件 SettingFrm.pas-----------用于系统的设置的单元文件 11)SiteArrangementFrm.dfm---整理“站内搜索”子模块中用户设置的窗体文件 SiteArrangementFrm.pas---整理“站内搜索”子模块中用户设置的单元文件 12)SiteSearchFrm.dfm--------主要用于实现站内搜索的窗体文件 SiteSearchFrm.pas--------主要用于实现站内搜索的单元文件 13)SplashFrm.dfm-----------实现系统开始运行闪屏的窗体文件 13)SplashFrm.pas-----------实现系统开始运行闪屏的单元文件 14)ViewFra.dfm--------------设定查询数据库保存的搜索结果信息条件输入的窗体文件 ViewFra.pas--------------设定查询数据库保存的搜索结果信息条件输入的单元文件 15)ViewInfoFrm.dfm----------对数据库中信息查询的窗体文件 ViewInfoFrm.pas-----------对数据库中信息查询的单元文件 16)UnitSearch.pas-----------实现百度、新浪等搜索引擎线程定义的单元文件 以上是code文件夹的主要文件,也是本案例工程的所有代码文件,其中ChannelPlayer.dpr是工程文件,记录本工程的信息;其他的窗体都是在本系统程序使用时动态调用(各个文件的调用关系可以参考本书第一章实例分析与完善的概要模块设置部分)。code文件夹中其他文件是在程序编译运行时候有delphi7自动生成,用户可以不管。其中*.~后缀的文件是相应文件名的备份,它们也是由delphi7自动生成。 (三)source文件夹中dcu文件夹中保存的文件是程序在编译的时候生成的中间文件,它们都对应code文件夹中的每个*.pas单元文 件。 (四)source文件夹中exe文件夹的文件: ChannelPlayer.exe---是本项目工程运行是自动生成的可执行运用程序 (五)系统需求: 1. 硬件要求: 基本配置为: ¢ CPU:Intel Pentium II-class 300 MHz (Intel Pentium III-class 600 MHz recommended) 这表明需要至少300MHz的奔III处理器,笔者所使用的是雷鸟1G,应该说 性能还是可以的。 ¢ RAM:96MB(128MB recommended) 实际上,128M内存运行起来还是觉得不够,最好能够有256M以上内存。笔 者使用的是256M DDR内存。 ¢ Available hard disk space(for install):250MB ¢ Available hard disk space(post install):155MB 事实上,这个要求仅仅是针对安装delphi7所提出的 要求。 ¢ Video:800×600,256 colors 只要是14英寸显示器就可以达到这个要求。 ¢ CD-ROM:required 这只是目前计算机的标准配置,实际上,如果不是采用光盘安装的话,CD-ROM 根本就派不上用场。 ¢ Operating System:Microsoft Windows 2000(or up) ¢ Microsoft Internet Explorer 5.5(or up) 2. 本系统工程的开发环境 本信息搜索系统程序是在WindowsXP的Delphi7环境下开发编写的,并且通过测试。另外本系统也能在windows98,me,2000 的delphi7环境下开发编写。对于delphi的版本,虽然delphi6与delphi7相差不大,但由于本系统的网络部分用到的一些网络控件在delphi6没有,所以如果想使用delphi6编写的程序员要用delphi6的网络控件代替delphi7的网络控件。对于刚出的delphi8,由于它是基于net框架的,主要用于网页编写,编写应用程序的方式不一样,而且delphi7的很多控件都没有,所以一般不能在delphi8中开发运行(除非重新编写所有的代码)。 (六)注意事项: 本系统要注意开发环境的选取,如上面所说的,最好使用delphi7以下的版本,不能使用delphi8。另外还要注意系统程序所用到的数据库的设置,先是把数据库还原,然后设置TEST.UDL里的连接参数,使程序能与数据库建立关系。这样之后才能运行程序。最后,由于本系统是借助几大门户网站的搜索功能实现搜索,所以要注意这些门户网站的更新,根据它们的更新来更新本系统。 (七)技术支持信息: 本系统的运行与操作: 本系统在运行之前要先设置好与数据库的连接(参考第4章的实例分析与完善)。然后运行程序会出现一个主界面,界面中间是有关新闻的滚动。点击“定制搜索”功能可进入定制搜索界面,在界面的左边可以选择是搜索以前的关键字还是重新输入,如果重新输入着在“关键字”栏里填写关键字,然后选择相关设置就可以搜索。对于搜索到的信息可以直接单击进入网页,也可以右键点击选择不同的操作。保存搜索信息公能用于对本次搜索信息保存到数据库中。点击主界面的“查看信息”功能可以进入查看信息界面,其界面与定制搜索界面相识。 点击主界面的“滚动新闻”功能和“系统设置”功能可以设置主界面的新闻滚动和本系统的信息。 _________________________________________________________________ 技术支持的联系方式: 如果用户对于本系统程序有什么疑问可以发邮件到: hsw_gm@21cn.com tenny_2000@163.com sqwen@yeah.net kukocpoplee@tom.com Jingfei2000@21cn.com _________________________________________________________________
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值