Sådan: Moderniserede AngularJS 1.5+ med ES6, Webpack, Mocha, SASS og komponenter

Der er mange grunde til, at du måske ønsker at fortsætte med at arbejde med AngularJS 1.x, så jeg vil ganske enkelt antage, at du har dine grunde.

Angular! == AngularJS “Dette websted og alt dets indhold henviser til AngularJS (version 1.x), hvis du leder efter det nyeste Angular, kan du besøge angular.io” - angularjs.org

For nye projekter vil jeg anbefale at bruge React, da det er her momentumet er i frontend-udvikling.

Jeg lavede en GitHub-repo, du kan gaffel / klone for at starte dit eget projekt:

jsdoc_output // hvor dokumenter genereres
node_moduler // hvor dine sælgers ting går
Gitignore
mocha-webpack.opts // angive en anden webpack-konfiguration til test
package.json
README.md
webpack.config.base.js
webpack.config.js // udvider basiskonfiguration
webpack.config.test.js // udvider base config
offentlig
| index-bundle.js // webpack-genereret bundt
| index.html
| index.js // webpack
|
\ --- superAwesomeComponent
        componentStylez.sass
        componentTemplate.html
        fancyJsModule.js
        theComponent.js
        theComponent.spec.js
        theComponentController.js

Genereres ved hjælp af træ / a / f på windows

Lad os tjekke indeks.html


  
  
  
  
  

    En variabel på controlleren over komponenterne: {{IndexCtrl.fancyValue}}      

Der er ikke foretaget nogen handling endnu

Du kan se her, at vores to knapper er de to super-awesome-komponentelementer. Disse er Angular 1.5-komponenter.

Vinkelformede 1,5 komponenter

Vinkelformede 1,5-komponenter er bare direktiver med bedre standardværdier. De er altid elementer, der er en standard “Controller som $ ctrl”, og de har isoleret rækkevidde. Det meste af det, jeg lærte af komponenter, lærte jeg her https://toddmotto.com/exploring-the-angular-1-5-component-method/

Komponenterne har to bindinger, nogle input og nogle output.

Disse komponenter er nyttige, fordi de tillader os at indkapsle en kombination af visning og controller-funktionalitet. Lad os se på komponentfilen

importskabelon fra './componentTemplate.html'
import componentStylez fra './componentStylez.sass'
import {ComponentController} fra './theComponentController.js'
const bindinger = {
  someInput: '<',
  nogle Output: '&'
}
eksport const theComponent = {
  controller: ComponentController,
  skabelon,
  bindinger
}

Bemærk, hvordan hvert element i controlleren kunne genanvendes. Controlleren kan være specifik for denne komponent, eller det kan være en controller, der bruges andre steder.

Desuden indeholder denne fil referencer til alt hvad du har brug for at vide om, hvordan komponenten er. Komponenten er helt selvforsynende, du behøver ikke at bekymre dig om, hvordan den bruges i den større applikation for at gøre den.

Controlleren bruger normale ES6-funktioner, og jeg vil ikke undersøge, hvordan det fungerer, men bemærk bare den anvendte klassestruktur og manglen på $ omfang. Resultatet er en ramme-agnostisk controller, minus brug af komponentens livscyklushændelse ($ onInit)

import fancyFunction fra './fancyJsModule.js'
/ **
 * Tilbyder håndterere til komponenten
 * /
klasse ComponentController {
  / **
   * Meddeler, at inputbindinger ikke er defineret
   * @ return {undefined} undefined
   * /
  konstruktør () {
    console.log ('inputbindinger er ikke defineret!', this.someInput)
  }
  / **
   * Opkalder nogle Output med værdien af ​​someInput sat i fancyFunction
   * @ return {undefined} undefined
   * /
  doSuperThings () {
    console.log ('gør super ting')
    this.someOutput ({value: fancyFunction (this.someInput, 3)})
  }
  / **
   * Meddeler, at inputbindinger er defineret
   * @ return {undefined} undefined
   * /
  $ onInit () {
    console.log ('inputbindinger er defineret!', this.someInput)
  }
}
eksport {ComponentController}

StandardJS-formatering

Den åbenlyse forskel er manglen på semikoloner. Jeg personligt mener, at dette giver renere kode, og problemerne omkring semikolonanvendelse løses pænt ved hjælp af StandardJS-linter / formater, som forhindrer dig i at støde på underlige problemer der.

Webpack (hvilket kan være forvirrende)

Bemærk, hvordan vi kun skal importere index.bundle.js i index.html. Dette skyldes, at vi bruger Webpack, der samler alle vores aktiver i en enkelt fil. Dette inkluderer vores skabeloner, javascript, css og alt, hvad du kan forestille dig, at du har brug for derinde.

Webpack er et griset udyr, og et udyr det er. Det er kompliceret nok, at folk sætter det på deres CV. Det flytter en masse kompleksitet fra forskellige dele af din applikation til din webpack.config.js-fil.

Bevis for denne kompleksitet kan findes i det faktum, at vi har årsag til 3 webpack.config * .js-filer. Den ene giver en base, den anden er at rumme vores testopsætning, og den tredje er for at opdele kode i leverandørstykker (hvilket vi ikke ønsker at gøre i vores testopsætning, for at underlige interaktioner med CommonsChunkPlugin).

var sti = kræver ('sti')
var webpack = kræve ('webpack')
module.exports = {
  indgang: {
    'index': path.join (__ dirname, '/public/index.js')
  },
  output: {
    filnavn: '[navn] -bundle.js',
    sti: path.join (__ dirname, '/ public /'),
    devtoolLineToLine: sandt,
    pathinfo: sandt,
    sourceMapFilename: '[navn] .js.map',
    publicPath: path.join (__ dirname, '/ src / main / webapp /')
  },
  modul: {
    læssere: [
      {test: /\.js$/, loader: 'babel-loader', ekskluder: / node_modules /},
      {test: /\.css$/, loader: 'style-loader! css-loader'},
      {test: /\.sass$/, loaders: ['style-loader', 'css-loader', 'sass-loader']},
      {test: /\.html$/, loader: 'raw-loader'},
      // inline base64 URL'er til <= 8k billeder, direkte URL'er til resten
      {test: /\.(png|jpg)$/, loader: 'url-loader? limit = 8192'},
      // hjælper med at indlæse bootstraps css.
      {test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url? limit = 10000 & minetype = application / font-woff'},
      {test: /\.woff2$/,
        loader: 'url? limit = 10000 & minetype = application / font-woff'},
      {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url? limit = 10000 & minetype = application / octet-stream'},
      {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'fil'},
      {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url? limit = 10000 & minetype = image / svg + xml'}
    ]
  },
  plugins: [
    ny webpack.HotModuleReplacementPlugin ()
  ],
  devServer: {
    publicPath: '/',
    contentBase: path.join (__ dirname, '/ public'),
    komprimering: sandt
  },
  devtool: 'eval'
}

Jeg vil ikke forklare alt her, for det er det, webpack-dokumenterne er til (dette link er til Webpack 1, selvom vi bruger Webpack 2. Webpack 2-dokumenterne er kun grundige i deres ufuldstændighed, men ser migrationerne dokumentation).

For at give et overblik skal du specificere:

  • Hvor din ansøgning starter
  • Hvor bundtet går
  • Hvordan du magisk importerer ting
  • Hvilke plugins bruger du
  • Din webpack-dev-serveropsætning
  • Sådan konfigureres dine kildekort.

Hvad? Plugins? Kildekort? Hvorfor har jeg brug for en anden server?

plugins

Her bruger vi bare HotModuleReplacement (HMR) plugin. Det giver vores browser mulighed for automatisk at genindlæse, når en fil ændres. Dette fjerner magisk et trin i den normale iteration af skriv, gem, test.

Der er masser af andre plugins derude (en, jeg ikke har fundet ud af at prøve, der skiller sig ud: https://github.com/owen-it/ng-loader)

Her er en liste over populære Webpack-plugins: https://github.com/webpack-contrib/awicious-webpack (hvorfor gør Webpack så mange ting?)

Kildekort

Kildekort er et produkt af ES6 og bundling. Jeg har ikke fundet ud af, hvordan man får dem til at være perfekte endnu, der er en uheldig hastighed / kvalitetsudveksling, der opstår med sourcemaps, da de perfekte kan være ret langsomme at skabe. Vores ES6-konvertering opnås gennem en babel-læsser.

Hvis vi ser tilbage på theComponent.js, indeholder dette det meste af vores Webpack

importskabelon fra './componentTemplate.html'
import componentStylez fra './componentStylez.sass'
import {ComponentController} fra './theComponentController.js'
const bindinger = {
  someInput: '<',
  nogle Output: '&'
}
eksport const theComponent = {
  controller: ComponentController,
  skabelon,
  bindinger
}

Bemærk, hvordan vi importerer html, SASS og ES6 her. Dette opnås gennem vores læsere. Hvilken læsser der bruges er baseret på filnavnet.

Webpack-dev-server

Webpack-dev-server er en forbløffende ting, uanset om du har en reel backend eller ej. Det understøtter HMR og er en statisk filserver, der gør din udvikling hurtig. Derudover vil brug af webpack-dev-server tvinge dig til at fjerne din frontend og backend.

At være i stand til at gøre frontend-udvikling uden at have brug for en "rigtig" server er fantastisk af mange grunde. Det vil tvinge dig til at oprette praktiske hånddata, vide nøjagtigt, hvilken funktionalitet der hører hjemme i backend vs. frontend, give dig HMR og gøre din frontend værtbar på næsten enhver server med en klar kontrakt mellem frontend og backend.

I denne opsætning køres webpack-dev-server sammen med alt andet, der er nødvendigt til frontend-udvikling, af en enkelt npm run dev-kommando, som specificeret i package.json

{
  "name": "moderne-angularjs-starter",
  "version": "0.0.1",
  "beskrivelse": "Baseprojekt",
  "main": "index.js",
  "scripts": {
    "dev": "samtidigt - kill-others \" webpack-dev-server - host 0.0.0.0 \ "\" npm kør dokumenter \ "",
    "docs_gen": "jsdoc -r -d jsdoc_output / public /",
    "docs_watch": "se \" npm køre docs_gen \ "public",
    "docs_serve": "ekko Dokumenter vises på port 8082! && live-server -q --port = 8082 --no-browser jsdoc_output /",
    "docs": "samtidigt - dræbe andre \" npm kør docs_serve \ "\" npm kør docs_watch \ "",
    "postinstall": "bower install",
    "webpack": "webpack",
    "test": "mocha-webpack public / ** / *. spec.js"
  },
  "devDependences": {/ * skjult for plads * /}
  "afhængigheder": {/ * skjult for plads * /}
}

Bemærk brugen af ​​samtidigt (https://www.npmjs.com/package/concurrently)

Dette giver os mulighed for at køre 2 blokerende kommandoer parallelt.

Bemærk, at der også er test- og dokumentationskommandoer. Dokumentationskommandoerne genererer JSDoc-sider og hostes derefter på en lille server, der automatisk opdateres (ligner HMR) browseren, når der sker en ændring. På denne måde kan du se dine dokumenter opdateres, mens du skriver dem, hvis du ofte gemmer.

Det er ikke demonstreret i dette projekt, men det at specificere typer i JSDoc er en god måde at specificere datakontrakter mellem frontend / backend. Alternativt kan du bare bruge typeskript (der er loadere til det).

Enhedstest: (fordi det er værd at gøre det)

Test med ES6 + AngularJS + Webpack er vanskeligt at komme rigtigt. Hver af disse forårsager komplikationer. Til enhedstestning sluttede jeg med at slå mig ned på meget små enheder og testede mine AngularJS-controllere som funktioner i Node. Karma er ret populær, men efter min mening er testene ikke rigtig enhedstest. Det ville imidlertid være nyttigt at have begge dele.

Vi har således mokka-webpakke. Dette giver os mulighed for at bruge import i vores test uden at specificere et indgangspunkt for hver enkelt.

Den sværeste del ved testning her er hån mod ES6-import. Der er nogle få forskellige måder at gøre dette på, men den eneste, der ikke krævede at ændre den fil, der testes, var injektion-loader.

Dette er især nyttigt til at skrive test, hvor hånlige ting i din modul-under-test undertiden er nødvendige før udførelse. - injektionslæsser
/ * eslint-disable * /
importer chai fra 'chai'
importer sinon fra 'sinon'
const theControllerInjector = kræve ('inject-loader! ./ theComponentController.js')
lad {forventer, skulle, hævde} = chai
beskriv ('superAwesomeComponent', funktion () {
  lad stubbe
  lad theComponentController
  lad controller
beforeEach (funktionsopsætningKomponent () {
    stub = sinon.stub (). returnerer (1)
    theComponentController = theControllerInjector ({
      // Modulet er virkelig enkelt, så det er ikke rigtig nødvendigt at spotte det
      // I en rigtig app kan det være meget mere kompliceret (dvs. noget der foretager API-opkald)
      './fancyJsModule.js': stub
    }). ComponentController
    controller = ny theComponentController ()
    controller.someOutput = sinon.stub ()
    controller.someInput = 1
  })
  beskriv ('doSuperThings', funktion () {
    it ('kalder fancyFunction', funktion () {
      controller.doSuperThings ()
      hævde (stub.calledOnce)
    })
  })
})

For at bruge inject-loader bruger vi den gamle kræver + webpack loader-syntaks, fordi der ikke er et jokertegn filnavnetjek, som vi kan gøre for importen (vi ønsker ikke, at alle js-filer skal sendes ind i inject-loader hele tiden) . Returnering af dette kræver giver os en funktion, som vi kan kalde med et objekt, der stopper forskellige importer ud:

theComponentController = theControllerInjector ({
  './fancyJsModule.js': stub
}). ComponentController

Her fjerner vi fancyJsModule fra vores controller's import. Dette giver os mulighed for at returnere en mock-værdi og undergrave al den logik, som modulet muligvis gør, så vi kan isolere eventuelle problemer, der opstår i testen.

Vi bruger chai som vores påståelsesbibliotek, sinon til hån / spionering og mokka til at køre testene.

Denne test forsøger ikke at være et godt eksempel på, hvad man skal teste, det er blot for at vise, hvordan test kan indstilles med ES6 + Webpack + Mocha + Angular.

Målet med dette er at tvinge udvikleren til at fokusere på at skrive AngularJS-handlere som faktiske funktioner. Der er en stærk tendens til, at disse håndterere udføres rent for bivirkninger, og at skabe disse test vil fremhæve denne kendsgerning.

Soo ...

Denne arkitektur giver en måde at modernisere en AngularJS-frontend uden at gøre et rammerhopp. En af de største fordele ved denne tilgang er, at den abstraherer en masse af den AngularJS-specifikke kode.

Et af de vanskeligste elementer i denne tilgang er at beslutte, hvad AngularJS-moduler skal bruges til vs. hvad man skal bruge ES6-moduler til. Jeg prøver at foretrække ES6 så meget som muligt. Dette skulle gøre det lettere at port en applikation ved hjælp af denne arkitektur til en anden ramme.

AngularJS har stadig en rimelig mængde liv i sig, men der er ingen debat om, at det er forbi sin førende. ES6 / 7 er dog stadig stigende (http://vanilla-js.com).

Længe leve AngularJS!

Forresten, tjek mine tidligere rants på JS https://medium.com/@narthur157/let-s-talk-about-javascript-bdb0bdf57fae