ASP.NET MVC 從零開(kāi)始 - 自動(dòng)化部署(其二)
這篇文章是從我的 github 博客 http://lxconan.github.io 導(dǎo)入的。
這是這個(gè)系列的第五篇了,前四篇請(qǐng)參見(jiàn):
- ASP.NET MVC 從零開(kāi)始 – Create and Run
- ASP.NET MVC 從零開(kāi)始 – Web.config
- ASP.NET MVC 從零開(kāi)始 - 請(qǐng)求處理
- ASP.NET MVC 從零開(kāi)始 - 自動(dòng)化部署(其一)
簡(jiǎn)單來(lái)說(shuō),部署就是 “構(gòu)建(Build)” -> “拷貝(打包)” -> “配置”。在前一篇中,我們介紹了“構(gòu)建”,那么這一篇就說(shuō)說(shuō)拷貝(好像我們更習(xí)慣于說(shuō)打包,那么以后我們就叫它打包吧)的事情。為什么要打包呢?在應(yīng)用程序發(fā)布的時(shí)候我們當(dāng)然只希望發(fā)布運(yùn)行時(shí)需要的文件,而其他的文件,例如:工程文件,源代碼等等是不需要進(jìn)行發(fā)布的。因此我們需要將運(yùn)行時(shí)所需的文件分離出來(lái),做成一個(gè)干凈的 Package。
打包 - 思路
只需要解決楚兩個(gè)問(wèn)題,打包就完成了:第一個(gè)問(wèn)題是,我們打的包應(yīng)該有怎樣的目錄結(jié)構(gòu);第二個(gè)問(wèn)題是,應(yīng)該拷貝哪些文件夾到包的哪些目錄里去。
應(yīng)該拷貝哪些文件
在回答第一個(gè)問(wèn)題之前,我們先來(lái)看看有哪些文件需要進(jìn)行拷貝。構(gòu)建好的程序集(.dll 和 .exe)需要拷貝,沒(méi)錯(cuò),但是除了它們以外還有其他文件需要進(jìn)行拷貝。如果在 Visual Studio 中打開(kāi) Web Project,并觀察每一個(gè)文件的 Build Action 屬性,你會(huì)發(fā)現(xiàn)幾乎所有的文件都屬于以下四種 Build Action:
- None:這意味著這個(gè)文件在構(gòu)建過(guò)程中將不做任何處理。典型的例子是 Readme 或者 EULA(End User License Agreement) 文件。這種文件不會(huì)在打包中進(jìn)行拷貝;
- Compile:這類文件會(huì)在構(gòu)建過(guò)程中進(jìn)行編譯,編譯結(jié)果會(huì)嵌入到生成的程序集(dll 或者 exe)中。這類文件在打包的時(shí)候是不會(huì)進(jìn)行拷貝的;
- Content:這個(gè)文件不會(huì)在構(gòu)建過(guò)程中進(jìn)行編譯。但是這個(gè)文件屬于整個(gè)工程發(fā)布的一個(gè)部分。因此這類文件在打包的時(shí)候會(huì)進(jìn)行拷貝;
- Embedded Resources:這個(gè)文件的內(nèi)容將作為一種嵌入式資源在構(gòu)建過(guò)程中嵌入到程序集中。這個(gè)文件在打包的過(guò)程中不會(huì)被拷貝;
因此,除了構(gòu)建好的程序集之外,所有 Build Action 為 Content 的文件類型也會(huì)在打包的時(shí)候被拷貝。
以我們的工程為例:
FromZero.App
│ Global.asax [Content]
│ Global.asax.cs [Compile]
│ packages.config [Content]
│ Web.config [Content]
│ Web.Debug.config [None]
│ Web.Release.config [None]
│
├─bin
│ /* All build results are stored in this directory. */
│
├─Controllers
│ HomeController.cs [Compile]
│
├─Properties
│ AssemblyInfo.cs [Compile]
│
└─Views
└─Home
Index.cshtml [Content]
那么需要拷貝的文件為:
- .\bin 文件夾下的所有文件;
- 所有 Build Action 為 Content 屬性的文件:Global.asax、packages.config、Web.config、Index.cshtml。
包的目錄結(jié)構(gòu)
在上一節(jié)我們介紹了,所有構(gòu)建生成的程序集和 Build Action 為 Content 的文件都會(huì)在打包過(guò)程中進(jìn)行拷貝。那么它們會(huì)拷貝到什么地方去呢?答案是拷貝到相應(yīng)的目錄下面去。以我們的工程為例,假設(shè)我們希望將構(gòu)建好的工程拷貝到一個(gè)名為 Package 的目錄下去,那么這個(gè) Package 目錄在打包完畢之后應(yīng)該是這個(gè)樣子的:
Package
│ Global.asax
│ packages.config
│ Web.config
│
├─bin
│ /* All build results. */
│
└─Views
└─Home
Index.cshtml
等一下,Controller 和 Properties 目錄到哪里去了?由于這兩個(gè)目錄下面沒(méi)有一個(gè)文件需要進(jìn)行發(fā)布,因此這個(gè)目錄也就不會(huì)創(chuàng)建。
假設(shè)你的確需要一個(gè) Controller 目錄進(jìn)行發(fā)布,該怎么辦呢?那么我們可以利用規(guī)則創(chuàng)建一個(gè) 0KB 的 placeholder 文件。并且將這個(gè)文件的 Build Action 屬性設(shè)置為 Content。
至此我們已經(jīng)可以總結(jié)出打包的規(guī)則了:
- 拷貝所有構(gòu)建過(guò)程中生成的程序集文件,以及 Build Action 為 Content 的文件;
- 將所有需要拷貝的文件拷貝到一個(gè)和其所在的工程目錄對(duì)應(yīng)的目錄下面,如果某一個(gè)目錄下沒(méi)有一個(gè)文件需要在打包中進(jìn)行拷貝,則不生成這個(gè)目錄。
打包-代碼
我們是否需要自己解析工程的 XML 結(jié)構(gòu)然后按照上述規(guī)則進(jìn)行打包呢?幸運(yùn)的是,完全不用:這是因?yàn)樵?ASP.NET Web 工程中會(huì)引用 $(VSToolsPath)\Web\Microsoft.Web.Publishing.targets,其中定義的 _WPPCopyWebApplication 過(guò)程正是我們以上描述的過(guò)程。我們只需要在上一個(gè)例子的基礎(chǔ)上修改 Compile-Project 函數(shù):
Function Compile-Project() {
iex -Command "& '$global_msBuildPath' /t:Rebuild /t:_WPPCopyWebApplication /p:WebProjectOutputDir='$global_buildDirPath\Package\' /p:UseWPP_CopyWebApplication=True /p:PipelineDependsOnBuild=False '$project_path'"
}
其中:
$global_msBuildPath是 msbuild.exe 的所在位置;/t:Rebuild:首先執(zhí)行 Rebuild 過(guò)程,這將刪除上一次的構(gòu)建結(jié)果,然后重新構(gòu)建整個(gè)項(xiàng)目;/t:_WPPCopyWebApplication:將該項(xiàng)目進(jìn)行打包;/p:WebProjectOutputDir='$global_buildDirPath\Package\':將整個(gè)打包結(jié)果存放在 buildDir 下的 Package 目錄下。如果這個(gè)目錄不存在則創(chuàng)建這個(gè)目錄;/p:UseWPP_CopyWebApplication=True:從 Visual Studio 2010 開(kāi)始,我們可以使用 Web.config.$(Configuration).config 文件對(duì) Web.config 在不同的編譯選項(xiàng)下進(jìn)行修正。為了使用能夠這個(gè)功能,需要設(shè)定此變量值為True;/p:PipelineDependsOnBuild=False:如果將UseWPP_CopyWebApplication設(shè)置為True,則必須將PipelineDependsOnBuild變量設(shè)置為False否則將導(dǎo)致 MSBuild 的 Targets 的循環(huán)引用。具體的技術(shù)細(xì)節(jié)請(qǐng)參見(jiàn)這里。
這么長(zhǎng)的一坨命令非常不容易維護(hù),因此我們可以將這些命令放在一個(gè) MSBuild 工程中。首先,我們建立一個(gè) XML 文件,不妨命名為 Deploy.xml:
<?xml version="1.0" encoding="utf-8"?>
<Project
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="12.0">
<Target Name="Build">
<MSBuild
Projects="..\src\FromZero.App\FromZero.App.csproj"
Targets="Rebuild;_WPPCopyWebApplication"
Properties="WebProjectOutputDir=$(WebAppPublishDir);UseWPP_CopyWebApplication=True;PipelineDependsOnBuild=False;"/>
</Target>
</Project>
這樣,我們只需要在 Compile-Project 函數(shù)中用 MSBuild 調(diào)用這個(gè) Deploy.xml 文件,并將希望的包的輸出目錄賦值給 $(WebAppPublishDir) 變量即可:
$global_deployProject = "$global_buildDirPath\deploy.xml"
Function Compile-Project() {
iex -Command "& '$global_msBuildPath' /p:WebAppPublishDir='$global_buildDirPath\Package\' '$global_deployProject'"
}
到現(xiàn)在,Compile-Project 函數(shù)已經(jīng)不止是在編譯工程了,它還具備了打包的能力,因此我們將其重命名為 Deploy-Project。
附:deploy.ps1 到目前為止的代碼
$ErrorActionPreference = 'Stop'
# Environment helpers ------------------------------------
Function Get-MsBuildPath() {
$msBuildRegPath = "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\12.0"
$msBuildPathRegItem = Get-ItemProperty $msBuildRegPath -Name "MSBuildToolsPath"
$msBuildPath = $msBuildPathRegItem.MsBuildToolsPath + "msbuild.exe"
return $msBuildPath
}
# Environment variables ----------------------------------
$global_buildDirPath = Get-Location
$global_msBuildPath = Get-MsBuildPath
$global_solutionPath = "$global_buildDirPath\..\src"
$global_solutionFilePath = "$global_solutionPath\src.sln"
$global_nugetPath = "$global_buildDirPath\tools\nuget.exe"
$global_deployProject = "$global_buildDirPath\deploy.xml"
# Install nuget packages ---------------------------------
Function Install-SolutionPackages() {
iex "$global_nugetPath restore $global_solutionFilePath"
}
$project_path = $global_solutionPath + '\FromZero.App\FromZero.App.csproj'
Function Deploy-Project() {
iex -Command "& '$global_msBuildPath' /p:WebAppPublishDir='$global_buildDirPath\Package\' '$global_deployProject'"
}
Install-SolutionPackages
Deploy-Project
浙公網(wǎng)安備 33010602011771號(hào)