Ako integrovať Visual Studio 2013 s Grunt-om - časť 4 final

DOWNLOAD

V predošlej časti sme si ukázali ako pri builde aplikácie volať rôzne Grunt tasky v závislosti od zvolenej konfigurácie priamo z Visual Studia. Build je jedna vec, publish je zas druhá.

Na build proces sme sa napojili tak, že sme doplnili naše vlastné MSBuild target-y do PropertyGroup BuildDependsOn. Jeden by čakal (teda aspoň ja som to čakal), že pre publish tu bude podobná property PublishDependsOn. Žial, nie je tomu tak. Aby sme sa mohli napojiť na publish proces, zadefinujeme si v súbore GruntMvc.targets nový target PublishTarget:

<Target Name="PublishTarget" 
        AfterTargets
="CopyAllFilesToSingleFolderForPackage" 
        BeforeTargets
="MSDeployPublish" 
        DependsOnTargets
="$(PublishTargetDependsOn)">
</Target>
PublishTarget

Tento target bude zavolaný potom, ako bol zavolaný target CopyAllFilesToSingleFolderForPackage, čo je target, ktorý MSBuild volá pri publikovaní projektu. Aby sme mohli tento target dopĺňať ďalšími target-mi, podobne ako sme to robili pri builde, zadefinujeme si vlastnú PropertyGroup PublishTargetDependsOn:

<PropertyGroup>
  
<PublishTargetDependsOn>
    HelloWorldTarget;
  
</PublishTargetDependsOn>
</PropertyGroup>
PropertyGroup PublishTargetDependsOn

Publish projektu spustíme z Visual Studia tak, že v okne Solution Explorer klikneme pravým tlačidlom myšky na náš projekt, z menu vyberieme možnosť
  • Publish...
. V novo otvorenom okne zvolíme možnosť <New Profile...> a zadáme názov pre náš publish profil (GruntMvcDemo-RELEASE) a potvrdíme stlačením tlačidla OK.

Vytvorenie publish profilu
Vytvorenie publish profilu

V záložke Connection zvolíme možnosť File System (môžeme využiť aj inú možnosť, pre jednoduchosť využijeme práve túto), a nastavíme Target location a potvrdíme stlačením tlačidla Next >:

Nastavenie connection pre publish profil
Nastavenie connection pre publish profil

V záložke Settings z menu Configuration vyberieme možnosť Release, zaškrtneme možnosť Delete all existing files prior to publish a potvrdíme stlačením tlačidla Next >:

Nastavenie konfigurácie pre publish profil
Nastavenie konfigurácie pre publish profil

V poslednom kroku už spustíme samotný publish stlačením tlačidla Publish:

Spustenie publikovania aplikácie
Spustenie publikovania aplikácie

Tento postup už pri každom ďalšom publikovaní nemusíme opakovať, nakoľko publish profil sa uloží do projektu a stačí v prvom kroku vybrať ten ktorý profil a rovno pustiť publish. Pri jeho spustení sa spustí build projektu v nami zvolenej konfigurácii a následne aj náš target PublishTarget.

V output okne si môžeme všimnúť log "Copying all files to temporary location below for package/publish obj\Release\Package\PackageTmp." za ktorým nasleduje log z nášho testovacieho target-u HelloWorldTarget. V podadresári projektu obj\Release\Package\PackageTmp sa nachádza výsledok publishu, ktorý MSBuild ešte nakopíruje do adresára, ktorý sme zadali pri vytváraní publish profilu do poľa Target location. Všetky zmeny, ktoré v tejto fáze publikovania vykonáme v adresári PackageTmp sa prejavia aj na konci procesu a teda aj v adresári Target location.

Teraz nastal čas na to, aby sme si ukázali, ako môžeme mať pre každú build konfiguráciu vlastnú konfiguráciu aplikácie. Vo Visual Studiu máme pre každý projekt by default dve konfigurácie: Debug a Release. V adresári js máme súbor config.js, ktorý predstavuje konfiguráciu aplikácie pre Debug mód. Pre Release si tento súbor skopírujeme (stačí použiť všemocnú klávesovú kombináciu Ctrl+C, Ctrl+V) a skopírovaný súbor premenujeme na config.Release.js. Aby táto konfigurácia bola aspoň v niečom odlišná od Debug-u, nastavíme konfiguračnú vlastnosť environment pre modul app na RELEASE:

require({
    paths: {
        jquery: 
"libs/jquery",
        domReady: 
"libs/domReady"
    
},
    shim: {
        
"jquery": {
            exports: 
"jQuery"
        
}
    },
    config: {
        
"app": {
            environment: 
"RELEASE"
        
}
    }
}, [
"app"]);
Obsah súboru config.Release.js

V tejto chvíli sa pri publikovaní projektu do výsledného adresára dostanú oba súbory: config.js a aj config.Release.js. Aby sme mali vo výsledku len jeden, vytvoríme si nový target RenamePublishConfigurationTarget a pridáme ho do PropertyGroup PublishTargetDependsOn:

<Target Name="RenamePublishConfigurationTarget">
  
<Move SourceFiles="$(_PackageTempDir)/js/config.$(Configuration).js" 
        DestinationFiles
="$(_PackageTempDir)/js/config.js" />
</
Target>
RenamePublishConfigurationTarget

Tento target premenuje javascriptový konfiguračný súbor (na základe použitej konfigurácie pri builde ktorá je v property Configuration) na súbor config.js.

Výsledkom publish akcie je teraz webová aplikácia, kde v adresári js máme všetky naše pôvodné javascript-y: app.js, config.js (ktorý vznikol premenovaním súboru config.Release.js), libs/require.js, libs/jquery.js a libs/domReady.js. Na týchto súboroch si teraz ukážeme, ako volať vlastný grunt task a to len pri publikovaní (nie počas build) pričom je možné tiež zohladniť zvolenú konfiguráciu vo Visual Studiu. Tieto grunt tasky budeme vykonávať v rámci adresára _PackageTempDir - nechceme totiž modifikovať pôvodné súbory, ale len tie ktoré sú výsledkom publish akcie. Keďže do tohto adresára sa neprenášajú nainštalované npm balíčky (adresár node_modules), pretože sme ich nepridali do projektu, musíme doplniť do PropertyGroup PublishTargetDependsOn ďalší target InstallNpmPublishPackagesTarget, ktorý ich nainštaluje aj v priečinku _PackageTempDir:

<Target Name="InstallNpmPublishPackagesTarget">
  
<Message Text="Installing npm publish packages:" Importance="high" />
  <
Exec WorkingDirectory="$(_PackageTempDir)" 
        Command
="call npm install > $(ConsoleOutputFile)" />
  <
Message Text="... done" Importance="high" />
</
Target>
InstallNpmPublishPackagesTarget

Podobne ako pri builde aj teraz pripravíme ďalší target GruntPublishTarget, ktorý spustí grunt task publish a prepošle mu ako parameter aj aktuálne zvolenú konfiguráciu, v našom prípad konfiguráciu Release. Tento target tiež doplníme do PropertyGroup PublishTargetDependsOn:

<Target Name="GruntPublishTarget">
  
<Message Text="Running grunt publish:" Importance="high" />
  <
Exec WorkingDirectory="$(_PackageTempDir)" 
        Command
="call grunt publish:$(Configuration)" />
  <
Message Text="... done" Importance="high" />
</
Target>
GruntPublishTarget

Keď teraz spustíme publish, tento skončí s chybou "Task "publish:Release" not found.". Nemáme ešte totiž zadefinovaný tento task v súbore gruntfile.js. Takže si ho zadefinujeme. Čo by mal ale robiť? V aplikácii sme použili kničnicu Require.js, ktorej súčasťou je aj optimizer r.js, vďaka ktorému môžeme našu aplikáciu (naše javascripty) minifikovať, pospájať, inline-úť referencované štýly atď. Do súboru package.json doplníme referenciu na potrebný npm balíček:

{
  
// ...
  
"devDependencies": {
    
"grunt""^1.0.1",
    
"grunt-contrib-clean""^1.0.0",
    
"grunt-contrib-csslint""^1.0.0",
    
"grunt-contrib-cssmin""^1.0.1",
    
"grunt-contrib-jshint""^1.0.0",
    
"grunt-contrib-requirejs""^1.0.0"
  
}
}
Referencia na balíček grunt-contrib-requirejs

V súbore gruntfile.js rozšírením volania grunt.initConfig doplníme konfiguráciu pre tento task (bližšie informácie o nastavenia pre r.js je možné nájsť tu):

grunt.initConfig({
    
//...
    
requirejs: {
        publish: {
            options: {
                appDir: 
"./",
                
// Nechceme robit automaticku 
                // optimalizaciu celeho adresara
                
skipDirOptimize: true,
                writeBuildTxt: 
false,
                
// Kde sa nachadza konfiguracia aplikacie
                
mainConfigFile: "./js/config.js",
                dir: 
"./",
                keepBuildDir: 
true,
                
// Povodne subory chcem nechat prepisat
                
allowSourceOverwrites: true,
                
// Subory zakombinovane do inych suborov
                // nechame odstranit
                
removeCombined: true,
                preserveLicenseComments: 
false,
                optimize: 
"uglify",
                inlineText: 
true,
                
// Nechcem robit optimalizaciu css
                
optimizeCss: "none",
                skipModuleInsertion: 
false,
                
// Budeme optimalizovat modul
                
modules: [{
                    
// Nazov vysledneho modulu
                    
name: "config",
                    create: 
true,
                    
// Zacat od modulu config
                    
include: [
                        
"config"
                    
]
                }]
            }
        }
    },    
})
;
Konfigurácia pre task requirejs

a pridáme referenciu na tento task:

grunt.loadNpmTasks("grunt-contrib-requirejs");
Pridanie referencie na task grunt-contrib-requirejs

Do gruntfile.js doplníme ešte definíciu pre task publish, ktorý je v podstate kópia pôvodného build tasku:

// Publish task
grunt.registerTask("publish"function (target) {
    
if (!target) {
        grunt.fail.fatal(
"Missing publish configuration.");
        return;
    
}

    
// List of default tasks for each build configuration
    
var tasks [
    ]
;

    
// Custom task according to custom build configurations
    
switch (target.toUpperCase()) {
        
case "RELEASE":
            tasks.push(
"requirejs:publish");
            break;
        default
:
            grunt.fail.fatal(
"Unknown publish configuration '" + target + "'");
            break;
    
}

    
// Run all tasks
    
grunt.task.run.apply(grunt.task, tasks);
});
Definícia grunt task-u publish

Výsledkom volania tohto tasku bude stav, že v adresári js ostanú už len dva súbory /libs/require.js a súbor config.js v rámci ktorého budú obsiahnuté všetky ostatné javascriptové súbory, ktoré boli naším modulom app referencované, súbor bude navyše minifikovaný a referencované súbory budú odstránené.

Výsledný publish folder obsahuje ešte niekoľko ďalších súborov (ConsoleOutputFile.txt, packages.config, ...) a adresárov (node_modules), ktoré v skutočnosti nie sú potrebné pre samotnú aplikáciu. Tieto môžeme odstrániť pomocou ďalšieho targetu DeletePublishTempFilesTarget:

<Target Name="DeletePublishTempFilesTarget" >
  
<Message Importance="high" Text="Deleting publish temp files:" />
  <
Exec WorkingDirectory="$(_PackageTempDir)"
        Command
="call rimraf node_modules > $(ConsoleOutputFile)" />

  <
Delete Files="$(_PackageTempDir)\$(ConsoleOutputFile)"/>
  <
Delete Files="$(_PackageTempDir)\packages.config" ContinueOnError="true"/>
  <
Delete Files="$(_PackageTempDir)\gruntfile.js" ContinueOnError="true"/>
  <
Delete Files="$(_PackageTempDir)\package.json" ContinueOnError="true"/>
  <
Message Text="... done" Importance="high" />
</
Target>
DeletePublishTempFilesTarget

Treba si všimnúť, že na vymazanie adresára node_modules sme použili príkaz rimraf a nie MSBuild task Delete. MSBuild task Delete sa nevie vysporiadať s adresármi, ktorých vnorenie je príliš veľké, a pri node balíčkoch to vnorenie naozaj veľké je. Preto použijeme ešte jeden target InstallRimrafCliTarget, ktorý nainštaluje potrebný balíček, podobne ako to robí InstallGruntCliTarget. Tento target doplníme do PropertyGroup BuildDependsOn:

<Target Name="InstallRimrafCliTarget">
  
<Message Text="Installing 'rimraf' package:" Importance="high" />
  <
Exec WorkingDirectory="$(ProjectDir)" 
        Command
="call npm install -g rimraf > $(ConsoleOutputFile)" />
  <
Delete Files="$(ConsoleOutputFile)"/>
  <
Message Text="... done" Importance="high" />
</
Target>
InstallRimrafCliTarget

A naozaj úplne posledná vec, ktorú treba ešte spraviť, aby naša integrácia bola kompletná, je zahrnúť do publish výstupu aj súbory, ktoré sú generované počas buildu, ale nie sú priamou súčasťou projektu. Hovorím o vygenerovaných css súboroch. Tieto sa generujú prostredníctvom grunt tasku do adresárovej štruktúry projektu, sú dostupné pri spustení aplikácie priamo s Visual Studia, ale nie sú pridané do samotného projektu. Proces publikovania počas svoju behu "zozbiera" všetky súbory, ktoré treba (ide hlavne o súbory ktoré sú v projekte pridané v ItemGroup-e Content) a tieto si odloží do ItemGroup FilesForPackagingFromProject. Za týmto účelom vytvoríme nový (ale už naozaj posledný) target IncludeCssFilesTarget:

<Target Name="IncludeCssFilesTarget">
  
<ItemGroup>
    
<FilesForPackagingFromProject Remove="css\**\*.css"/>
    <
_MinifiedCssFiles Include="css\**\*.min.css" />
    <
FilesForPackagingFromProject Include="%(_MinifiedCssFiles.Identity)">
      
<DestinationRelativePath>css\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
    
</FilesForPackagingFromProject>
  
</ItemGroup>
</Target>
IncludeCssFilesTarget

Tento target ale nepridáme do PropertyGroup PublishTargetDependsOn, lebo to by už bolo neskoro. Zozbieranie súborov pre publish prebieha v rámci tasku CopyAllFilesToSingleFolderForPackage, a preto musíme rozšíriť PropertyGroup CopyAllFilesToSingleFolderForPackageDependsOn, ktorá podobne ako pri iných target-och definuje targety, ktoré treba vykonať pred vykonaním target-u samotného:

<PropertyGroup>
  
<CopyAllFilesToSingleFolderForPackageDependsOn>
    IncludeCssFilesTarget;
    $(CopyAllFilesToSingleFolderForPackageDependsOn);
  
</CopyAllFilesToSingleFolderForPackageDependsOn>
</PropertyGroup>
Rozšírenie PropertyGroup CopyAllFilesToSingleFolderForPackageDependsOn

A to je už naozaj všetko. Kompletné vzorové riešenie je možné nájsť aj na mojom github-e, pričom nie je limitované použitím zrovna tých grunt taskov, ktoré som použil práve ja. Rovnako tak, ak niekomu vyhovuje viac napríklad gulp, može si daný projekt upraviť podľa svojich predstáv.

Publikované Monday, May 30, 2016 8:31 AM xxxmatko

Komentáre

Bez komentárov