React + Vite 应用环境搭建、部署备忘录

首先

我个人在开发中经常使用React。

然而,由于每次都会问自己“怎么来创建来着?”所以为了今后个人开发的顺利进行,我将其作为备忘录留下来。

只是列举命令、文件等内容,所以没有太多的解释说明。
请谅解。

创建项目

执行下列命令。

docker run --rm -it -v $PWD:/app -w /app --platform=linux/amd64 node:20-slim \
sh -c 'npm install -g npm && npm create -y vite@latest react-vite-sample -- --template react-swc-ts'   

我正在执行创建Docker容器并创建vite项目的命令。
由于我使用的是Apple芯片的Mac终端,所以我加上了–platform=linux/amd64参数。

请将 “react-vite-sample” 替换为任意项目名称。

创建与Docker相关的文件

文件夹层次结构

プロジェクトルート
├─ .devcontainer
|  └─ devcontainer.json
├─ docker-compose.yml
└─ Dockerfile

使用docker-compose,堆栈名称将成为父目录的名称。

过去我们将docker-compose.yml文件放在docker目录中,但是由于与其他项目的堆栈名称重复,所以现在我们将其放在项目根目录下。

文件

{
    "service": "react-vite-sample",
    "dockerComposeFile": "../docker-compose.yml",
    "workspaceFolder": "/workdir",
    "customizations": {
      "vscode": {
        "extensions": ["eamodio.gitlens", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
        "settings": {
          "prettier.configPath": ".prettierrc.json",
          "editor.defaultFormatter": "esbenp.prettier-vscode",
          "editor.formatOnSave": true,
          "editor.codeActionsOnSave": {
            "source.fixAll.eslint": true
          },
          "editor.tabSize": 2
        }
      }
    }
}
version: '3.9'
services:
  react-vite-sample:
    ports:
      - 5173:5173
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/workdir
    tty: true
FROM --platform=linux/amd64 node:20-slim

RUN apt-get update && apt-get install -y git vim locales-all

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8

WORKDIR /workdir

— 因为终端是苹果的Mac,所以我安装了平台为linux/amd64。

通过安装locales-all并使用ENV设置环境变量,在容器内使用git时,可以解决日语输入导致的乱码问题。

将上述内容放置在下面的命令中,即可在容器内进行提交等操作:通过设定”user.email”和”user.name”。

git config --global user.email <メールアドレス>
git config --global user.name <名前>

另外,还需要执行以下命令来安装库。

npm install

ESLint和Prettier的配置

文件夹结构

プロジェクトルート
├─ .eslintrc.cjs
├─ .prettier.json
├─ tsconfig.json
└─ vite.config.ts

文件

执行以下命令

npm install eslint prettier @typescript-eslint/eslint-plugin \
@typescript-eslint/parser eslint-config-prettier \
eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react \
eslint-import-resolver-typescript

修改 .eslintrc.cjs文件

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
+   'plugin:import/recommended',
+   'plugin:jsx-a11y/recommended',
+   'eslint-config-prettier',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh'],
+ settings: {
+   react: {
+     version: 'detect',
+   },
+   'import/resolver': {
+    typescript: {},
+   },
+ },
  rules: {
    'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+   'no-restricted-imports': ['error', { patterns: ['./', '../'] }],
  },
};

在项目文件夹的根目录下创建一个新的.prettier.json文件。

{
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "printWidth": 120,
  "bracketSpacing": true
}

我们将使src目录下的文件可以通过@/的形式进行导入。

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    
+   "baseUrl": "./",
+   "paths": {
+     "@/*": ["src/*"]
+   }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
+ resolve: {
+   alias: {
+     '@': '/src',
+   },
+ },
});

由于发生错误,我们将修复 main.tsx 和 App.tsx。

import React from 'react';
import ReactDOM from 'react-dom/client';
- import App from './App.tsx';
+ import App from '@/App.tsx';
- import '@/index.css';
+ import '@/index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
import { useState } from 'react';
- import reactLogo from './assets/react.svg';
+ import reactLogo from '@/assets/react.svg';
+ // eslint-disable-next-line import/no-unresolved
import viteLogo from '/vite.svg';
- import './App.css';
+ import '@/App.css';

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
    </>
  );
}

export default App;

考试准备

执行以下命令

npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom

在src目录下创建一个名为App.test.tsx的文件。
由于只有一个测试被描述,所以请根据需要进行添加和修改。

import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import App from '@/App';

test('renders h1 text', () => {
  render(<App />);
  const headerElement = screen.getByText(/Vite \+ React/);
  expect(headerElement).toBeInTheDocument();
});

将vite.config.ts文件更改如下。

+ /// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': '/src',
    },
  },
+ test: {
+   globals: true,
+   environment: 'jsdom',
+ },
});

在package.json文件中的script属性中添加test。

{
  "name": "test-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
+   "test": "vitest"
  },
  以下略...
}

创建CloudFormation模板

这是一个模板,使用CloudFront + S3进行托管,并通过将push推送到CodeCommit(在此配置中通过GitHub Actions镜像推送到CodeCommit)来触发CodePipeline,并进行部署。

我认为通过将”react-vite-sample”和”ReactViteSample”字符串替换为任何项目名称,可以轻松地将其应用到其他项目中。

文件夹结构

.
├─ cloudformation
│  ├─ parameters
│  │   ├─ codepipeline.json
│  │   ├─ hosting.json
│  │   └─ repository.json
│  ├─ shell
│  │   ├─ 01-repository.sh
│  │   ├─ 02-hosting.sh
│  │   ├─ 03-codebuild.sh
│  │   └─ 04-codepipeline.sh
│  └─ templates
│      ├─ codebuild.yml
│      ├─ codepipeline.yml
│      ├─ hosting.yml
│      └─ repository.yml
└─ buildspec.yml

参数

{
  "Parameters": {
    "RepositoryName": "react-vite-sample"
  }
}
{
  "Parameters": {
    "SourceBucketName": "react-vite-sample-source-bucket",
    "DestributionName": "react-vite-sample-destribution"
  }
}
{
  "Parameters": {
    "CodePipeLineName": "react-vite-sample-pipeline",
    "ArtifactBucketName": "react-vite-sample-pipeline-artifact-store",
    "EventBridgeName": "react-vite-sample-eventbridge-pipeline",
    "EventBridgeRoleName": "react-vite-sample-eventbridge-role",
    "EventBridgePolicyName": "react-vite-sample-eventbridge-policy"
  }
}

在文件名中添加01等字符,但只要CodePipeline最后执行,其余的顺序都是无关紧要的。

#!/bin/bash

CHANGESET_OPTION="--no-execute-changeset"

if [ $# = 1 ] && [ $1 = "deploy" ]; then
  echo "deploy mode"
  CHANGESET_OPTION=""
fi

CFN_TEMPLATE=../templates/repository.yml
CFN_STACK_NAME=react-vite-sample-repository

aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
    --parameter-overrides file://../parameters/repository.json
#!/bin/bash

CHANGESET_OPTION="--no-execute-changeset"

if [ $# = 1 ] && [ $1 = "deploy" ]; then
  echo "deploy mode"
  CHANGESET_OPTION=""
fi

CFN_TEMPLATE=../templates/hosting.yml
CFN_STACK_NAME=react-vite-sample-hosting

aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
    --capabilities CAPABILITY_NAMED_IAM \
    --parameter-overrides file://../parameters/hosting.json
#!/bin/bash

CHANGESET_OPTION="--no-execute-changeset"

if [ $# = 1 ] && [ $1 = "deploy" ]; then
  echo "deploy mode"
  CHANGESET_OPTION=""
fi

CFN_TEMPLATE=../templates/codebuild.yml
CFN_STACK_NAME=react-vite-sample-codebuild

aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
    --capabilities CAPABILITY_NAMED_IAM \
#!/bin/bash

CHANGESET_OPTION="--no-execute-changeset"

if [ $# = 1 ] && [ $1 = "deploy" ]; then
  echo "deploy mode"
  CHANGESET_OPTION=""
fi

CFN_TEMPLATE=../templates/codepipeline.yml
CFN_STACK_NAME=react-vite-sample-pipeline

aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION}\
    --capabilities CAPABILITY_NAMED_IAM \
    --parameter-overrides file://../parameters/codepipeline.json

模板

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  RepositoryName:
    Type: String

Resources:
  CodeCommitRepository:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref RepositoryName

Outputs:
  RepositoryName:
    Value: !Ref RepositoryName
    Export:
      Name: ReactViteSampleRepositoryName

  RepositoryArn:
    Value: !GetAtt CodeCommitRepository.Arn
    Export:
      Name: ReactViteSampleRepositoryArn
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SourceBucketName:
    Type: String

  DestributionName:
    Type: String

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref SourceBucketName
      VersioningConfiguration:
        Status: Enabled

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: Allow CloudFront Access
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub ${S3Bucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}

  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt S3Bucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity: ''
            OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
        Enabled: true
        DefaultRootObject: index.html
        Comment: !Ref DestributionName
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          CachePolicyId: !GetAtt CachePolicy.Id
          ViewerProtocolPolicy: redirect-to-https

  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: TestProject Origin Access Control
        Name: TestProjectOAC
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  CachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 0
        Name: TestProjectCachePolicy
        ParametersInCacheKeyAndForwardedToOrigin:
          CookiesConfig:
            CookieBehavior: none
          EnableAcceptEncodingGzip: false
          HeadersConfig:
            HeaderBehavior: none
          QueryStringsConfig:
            QueryStringBehavior: none

Outputs:
  URL:
    Value: !Sub https://${Distribution.DomainName}

  SourceBucketName:
    Value: !Ref SourceBucketName
    Export:
      Name: ReactViteSampleSourceBucketName

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
        Type: LINUX_CONTAINER
      Name: test-project-frontend-build
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        BuildSpec: buildspec.yml
        Type: CODEPIPELINE

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CodeBuildPolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - 's3:*'
                  - 'logs:*'

Outputs:
  CodeBuild:
    Value: !Ref CodeBuild
    Export:
      Name: ReactViteSampleCodeBuild
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  CodePipeLineName:
    Type: String

  ArtifactBucketName:
    Type: String

  EventBridgeName:
    Type: String

  EventBridgeRoleName:
    Type: String

  EventBridgePolicyName:
    Type: String

Resources:
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Ref CodePipeLineName
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactStoreBucket
      Stages:
        - Name: Source
          Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Configuration:
                RepositoryName: !ImportValue ReactViteSampleRepositoryName
                BranchName: main
                PollForSourceChanges: false
              Name: Source
              OutputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1

        - Name: Build
          Actions:
            - ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !ImportValue ReactViteSampleCodeBuild
              InputArtifacts:
                - Name: SourceArtifact
              Name: Build
              OutputArtifacts:
                - Name: BuildArtifact
              RunOrder: 1

        - Name: Deploy
          Actions:
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: 1
              Configuration:
                BucketName: !ImportValue ReactViteSampleSourceBucketName
                Extract: true
              Name: Deploy
              InputArtifacts:
                - Name: BuildArtifact
              RunOrder: 1
      RestartExecutionOnUpdate: false

  ArtifactStoreBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref ArtifactBucketName
      LifecycleConfiguration:
        Rules:
          - Id: clear-old-objects-rule
            Status: Enabled
            ExpirationInDays: 3
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CodePipelinePolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - 's3:*'
                  - 'codecommit:*'
                  - 'codebuild:*'

  EventBridge:
    Type: AWS::Events::Rule
    Properties:
      Description: for codepipeline
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - 'CodeCommit Repository State Change'
        resources:
          - !ImportValue ReactViteSampleRepositoryArn
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - main
      Name: !Ref EventBridgeName
      State: ENABLED
      Targets:
        - Arn: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
          Id: CodePipeline
          RoleArn: !GetAtt EventBridgeIAMRole.Arn

  EventBridgeIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - !Ref EventBridgeIAMPolicy
      RoleName: !Ref EventBridgeRoleName

  EventBridgeIAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - 'codepipeline:StartPipelineExecution'
            Resource:
              - !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
      ManagedPolicyName: !Ref EventBridgePolicyName

建筑规范.

version: 0.2

phases:
  install:
    on-failure: ABORT
    commands:
      - if [ -e /tmp/node_modules.tar ]; then tar xf /tmp/node_modules.tar; fi
      - npm install

  pre_build:
    on-failure: ABORT
    commands:
      - npm run test

  build:
    on-failure: ABORT
    commands:
      - npm run build

artifacts:
  files:
    - '**/*'
  base-directory: dist

cache:
  paths:
    - /tmp/node_modules.tar

创建 GitHub 存储库的设置,用于镜像

 

请参考上述文章,并执行以下操作。

    • SSHキーを作成

 

    • CodeCommitの公開SSHキーとして登録

 

    • GitHubリポジトリ作成

 

    作成したリポジトリのActionsのSecretsを設定

创建用于GitHub仓库镜像的文件

使用CodePipeline时,与CodeCommit的协作更加方便,但不能进行公开。
为了能够公开存储库,我们将GitHub存储库镜像到CodeCommit。

通过这样做,CodePipeline可以与CodeCommit进行协作,从而使配置变得更加简便。

创建CodeCommit存储库

请移动到 cloudformation/shell目录,并执行以下命令。
由于容器中未安装 aws-cli,因此请在已安装 aws-cli 的主机终端上执行。

sh 01-repository.sh deploy

在上述命令中创建了CodeCommit存储库,因此请在CodeCommit页面上复制SSH URL。

image.png

文件夹的层级

プロジェクトルート
└─ .github
   └─ workflows
      └─ main.yml

我将创建下面的内容。请将复制的URL粘贴到target_repo_url中。

name: Mirroring

on: [ push, delete ]

jobs:
  to_codecommit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 
      - uses: pixta-dev/repository-mirroring-action@v1
        with:
          target_repo_url: <コピーしたCodeCommitリポジトリのSSHのURL>
          ssh_private_key: ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY }}
          ssh_username: ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY_ID }}

创建服务 fu wù)

按顺序执行除了cloudformation/shell目录下的仓库之外的shell脚本。这也不是在容器内执行,而是在安装了aws-cli的主机终端上执行。

sh 02-hosting.sh deploy
sh 03-codebuild.sh deploy
sh 04-codepipeline.sh deploy

部署

当你将代码提交到GitHub仓库,它会与CodeCommit进行协作,并触发CodePipeline开始自动部署。

git init 
git commit -m "first commit"
git remote add <作成したgithubリポジトリのURL>
git push -u origin main

在部署后,您可以在CloudFront中选择相应的分发,将分发域名复制并粘贴到URL中,即可确认屏幕上的信息。

由于需要一些时间来反映,如果出现错误画面,请稍等一段时间后再确认一下。

最后

我认为使用Amplify可以很容易地进行构建,但由于不知道它在做什么,我感到有些害怕,所以我尝试进行了构建。

开发过程中,如果能够在开始时参考这篇文章并完成构建,那将是非常令人高兴的事情。

以下是GitHub存储库。
我认为最好是在适当的位置克隆并逐个复制所需的文件。

 

可以查看这篇文章。

ESLint是一个代码静态分析工具,用于在JavaScript代码中找到并修复问题。Prettier是一个代码格式化工具,可以自动调整代码的样式和排版。

    • Setting up ESLint & Prettier in ViteJS

 

    [import/no-unresolved] when using with typescript “baseUrl” and “paths” option #1485

云形成

    • CloudFormationの全てを味わいつくせ!「AWSの全てをコードで管理する方法〜その理想と現実〜」

 

    • CloudFormation で OAI を使った CloudFront + S3 の静的コンテンツ配信インフラを作る

 

    • CloudFormation】CloudFront の OAI を OAC に移行する

 

    • AWS CodeBuildでnode_modulesをキャッシュしたらハマった

 

    • CodePipelineをCloudFormationで作成してみた

 

    SPA(React)のビルドとデプロイを自動化するAWS CodePipeline用のCloudFormationテンプレート

测试

    • Vitest公式

 

    Vitestを使ってUnit Testingにチャレンジ(Vue, React, Svelte)

GitHub仓库镜像化

    GitHubにプッシュすると、CodeCommitへ自動で同期させる方法
bannerAds