這篇文章是從我的 github 博客 http://lxconan.github.io 導入的。
這是這個系列的第五篇了,前四篇請參見:
- ASP.NET MVC 從零開始 – Create and Run
- ASP.NET MVC 從零開始 – Web.config
- ASP.NET MVC 從零開始 - 請求處理
- ASP.NET MVC 從零開始 - 自動化部署(其一)
簡單來說,部署就是 “構建(Build)” -> “拷貝(打包)” -> “配置”。在前一篇中,我們介紹了“構建”,那么這一篇就說說拷貝(好像我們更習慣于說打包,那么以后我們就叫它打包吧)的事情。為什么要打包呢?在應用程序發布的時候我們當然只希望發布運行時需要的文件,而其他的文件,例如:工程文件,源代碼等等是不需要進行發布的。因此我們需要將運行時所需的文件分離出來,做成一個干凈的 Package。
打包 - 思路
只需要解決楚兩個問題,打包就完成了:第一個問題是,我們打的包應該有怎樣的目錄結構;第二個問題是,應該拷貝哪些文件夾到包的哪些目錄里去。
應該拷貝哪些文件
在回答第一個問題之前,我們先來看看有哪些文件需要進行拷貝。構建好的程序集(.dll 和 .exe)需要拷貝,沒錯,但是除了它們以外還有其他文件需要進行拷貝。如果在 Visual Studio 中打開 Web Project,并觀察每一個文件的 Build Action 屬性,你會發現幾乎所有的文件都屬于以下四種 Build Action:
- None:這意味著這個文件在構建過程中將不做任何處理。典型的例子是 Readme 或者 EULA(End User License Agreement) 文件。這種文件不會在打包中進行拷貝;
- Compile:這類文件會在構建過程中進行編譯,編譯結果會嵌入到生成的程序集(dll 或者 exe)中。這類文件在打包的時候是不會進行拷貝的;
- Content:這個文件不會在構建過程中進行編譯。但是這個文件屬于整個工程發布的一個部分。因此這類文件在打包的時候會進行拷貝;
- Embedded Resources:這個文件的內容將作為一種嵌入式資源在構建過程中嵌入到程序集中。這個文件在打包的過程中不會被拷貝;
因此,除了構建好的程序集之外,所有 Build Action 為 Content 的文件類型也會在打包的時候被拷貝。
以我們的工程為例:
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。
包的目錄結構
在上一節我們介紹了,所有構建生成的程序集和 Build Action 為 Content 的文件都會在打包過程中進行拷貝。那么它們會拷貝到什么地方去呢?答案是拷貝到相應的目錄下面去。以我們的工程為例,假設我們希望將構建好的工程拷貝到一個名為 Package 的目錄下去,那么這個 Package 目錄在打包完畢之后應該是這個樣子的:
Package
│ Global.asax
│ packages.config
│ Web.config
│
├─bin
│ /* All build results. */
│
└─Views
└─Home
Index.cshtml
等一下,Controller 和 Properties 目錄到哪里去了?由于這兩個目錄下面沒有一個文件需要進行發布,因此這個目錄也就不會創建。
假設你的確需要一個 Controller 目錄進行發布,該怎么辦呢?那么我們可以利用規則創建一個 0KB 的 placeholder 文件。并且將這個文件的 Build Action 屬性設置為 Content。
至此我們已經可以總結出打包的規則了:
- 拷貝所有構建過程中生成的程序集文件,以及 Build Action 為 Content 的文件;
- 將所有需要拷貝的文件拷貝到一個和其所在的工程目錄對應的目錄下面,如果某一個目錄下沒有一個文件需要在打包中進行拷貝,則不生成這個目錄。
打包-代碼
我們是否需要自己解析工程的 XML 結構然后按照上述規則進行打包呢?幸運的是,完全不用:這是因為在 ASP.NET Web 工程中會引用 $(VSToolsPath)\Web\Microsoft.Web.Publishing.targets,其中定義的 *_WPPCopyWebApplication* 過程正是我們以上描述的過程。我們只需要在上一個例子的基礎上修改 Compile-Project
函數:
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
:首先執行 Rebuild 過程,這將刪除上一次的構建結果,然后重新構建整個項目;/t:_WPPCopyWebApplication
:將該項目進行打包;/p:WebProjectOutputDir='$global_buildDirPath\Package\'
:將整個打包結果存放在 buildDir 下的 Package 目錄下。如果這個目錄不存在則創建這個目錄;/p:UseWPP_CopyWebApplication=True
:從 Visual Studio 2010 開始,我們可以使用 Web.config.$(Configuration).config 文件對 Web.config 在不同的編譯選項下進行修正。為了使用能夠這個功能,需要設定此變量值為True
;/p:PipelineDependsOnBuild=False
:如果將UseWPP_CopyWebApplication
設置為True
,則必須將PipelineDependsOnBuild
變量設置為False
否則將導致 MSBuild 的 Targets 的循環引用。具體的技術細節請參見這里。
這么長的一坨命令非常不容易維護,因此我們可以將這些命令放在一個 MSBuild 工程中。首先,我們建立一個 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
函數中用 MSBuild 調用這個 Deploy.xml 文件,并將希望的包的輸出目錄賦值給 $(WebAppPublishDir)
變量即可:
$global_deployProject = "$global_buildDirPath\deploy.xml"
Function Compile-Project() {
iex -Command "& '$global_msBuildPath' /p:WebAppPublishDir='$global_buildDirPath\Package\' '$global_deployProject'"
}
到現在,Compile-Project
函數已經不止是在編譯工程了,它還具備了打包的能力,因此我們將其重命名為 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
文章列表