В итоге я решил просто развернуть свое собственное консольное приложение на C #, чтобы автоматизировать весь процесс сборки моего сайта. Как это всегда происходит, сборка заняла гораздо больше времени, чем мне бы хотелось, но теперь одна команда переводит мой сайт прямо из Subversion в производство, поэтому я очень счастлив.
Во-первых, я использовал фантастический класс Mono.Options для обработки аргументов командной строки. Это единственный файл .cs , который вы можете просто добавить в свой проект и быть готовым к работе.
Я хотел получить аргументы командной строки, чтобы & mdash; например, & mdash; я мог указать, какую ревизию развернуть (если я не хотел HEAD).
using Mono.Options;
int rev = 0;
OptionSet opt = new OptionSet();
opt.Add("r=|revison=", "Revision to deploy (defaults to HEAD).", v => rev = int.Parse(v));
После того, как вы настроили все параметры, вы можете даже opt.WriteOptionDescriptions(Console.Out);
распечатать сообщение справки по использованию.
Я схватил SharpSvn для обработки экспорта SVN; на самом деле это было намного проще, чем ожидалось.
using SharpSvn;
SvnClient svn = new SvnClient();
svn.Authentication.DefaultCredentials = new System.Net.NetworkCredential("account", "password");
// Since this is an internal-only tool, I'm not too worried about just
// hardcoding the credentials of an account with read-only access.
SvnExportArgs arg = new SvnExportArgs();
arg.Revision = rev > 0 ? new SvnRevision(rev) : new SvnRevision(SvnRevisionType.Head);
svn.Export(new SvnUriTarget("<repository URL>"), workDir, arg);
... и весь сайт экспортируется во временную папку (workDir
). Поскольку я также хотел напечатать ревизию svn на сайте, я взял текущую ревизию репозитория (если ревизия не была указана).
SvnInfoEventArgs ifo;
svn.GetInfo(new SvnUriTarget("<repo URL>"), out ifo);
Теперь ifo.Revision
будет иметь ревизию HEAD.
Поскольку у меня был небольшой набор известных включаемых файлов, я решил просто загрузить их в память один раз, объединить номер ревизии, где это необходимо, и затем выполнить простое string.Replace
для каждого файла * .html в папке temp. .
string[] files = Directory.GetFiles(workDir, "*.html", SearchOption.AllDirectories);
foreach (string ff in files)
{
File.Move(ff, workDir + "_.tmp");
using (StreamReader reader = new StreamReader(workDir + "_.tmp"))
{
using (StreamWriter writer = new StreamWriter(ff))
{
string line;
while ((line = reader.ReadLine()) != null)
{
line = line.Replace("<!--#include virtual=\"/top.html\" -->", top);
// <etc..>
writer.WriteLine(line);
}
}
}
File.Delete(workDir + "_.tmp");
}
Переместите необработанный файл во временную папку, откройте StreamWriter
в исходном файле, прочитайте временный файл, замените известные <!--#include-->
s и удалите временный файл. Этот процесс завершается менее чем за секунду.
Еще одно, что я делаю, - минимизирую все мои скрипты и собираю их в один файл .js. Это позволяет мне поддерживать управляемость в процессе разработки (классы логически организованы в файлы), но оптимизировать все для производства. (Поскольку двадцать <script src="...">
тегов очень плохо .)
HTML Agility Pack был очень полезен для этой задачи. Я просто загрузил свой шаблон страницы в HtmlDocument
и извлек местоположения сценариев, которые нужно минимизировать и объединить в один файл. (Остальные файлы * .js в моем каталоге скриптов загружаются только на определенные страницы, поэтому я не хотел, чтобы они объединялись в основной файл.)
using HtmlAgilityPack;
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(top);
using (StreamWriter writer = new StreamWriter(workDir + "js\\compiled.js"))
{
foreach (HtmlNode script in doc.DocumentNode.SelectNodes("//script"))
{
string js = script.Attributes["src"].Value;
script.Remove();
js = js.Replace("/js/", workDir + "js/"); // In my site, all scripts are located in the /js folder.
js = js.Replace("/", "\\");
string mini;
if (js.IndexOf(".min.") > 0) // It's already minified.
{
mini = js;
}
else
{
mini = workDir + "_.tmp";
MinifyScript(js, mini);
}
using (StreamReader sr = new StreamReader(mini)) writer.WriteLine(sr.ReadToEnd());
File.Delete(js);
File.Delete(workDir + "_.tmp");
}
}
Затем найдите остальные сценарии для минимизации:
string[] jsfolder = Directory.GetFiles(workDir + "js\\", "*.js");
foreach (string js in jsfolder)
{
if (js.IndexOf("\\compiled.js") > 0) continue; // The compiled js file from above will be in the folder; we want to ignore it.
MinifyScript(js, js);
}
Для фактического минимизации я просто использовал YUI Compressor , который является jar-файлом Java. Вы можете заменить свой компрессор по вашему выбору здесь.
static void MinifyScript(string input, string output)
{
System.Diagnostics.ProcessStartInfo si = new System.Diagnostics.ProcessStartInfo(@"C:\Program Files (x86)\Java\jre6\bin\java.exe", "-jar mini.jar -o " + output + " " + input);
si.RedirectStandardOutput = true;
si.UseShellExecute = false;
System.Diagnostics.Process proc = System.Diagnostics.Process.Start(si);
proc.WaitForExit();
if (proc.ExitCode != 0) throw new Exception("Error compiling " + input + ".");
}
В моем процессе сборки этап минимизации фактически выполняется до обработки включений (поскольку я уменьшил количество тегов <script>
до одного в шаблоне).
Наконец, я использую Microsoft.Web.Administration.ServerManager
, чтобы временно остановить IIS, в то время как я перемещаю все файлы в моей временной папке в фактическую папку сайта prouction. (Я хотел предотвратить любые странности, пока сайт находится в полуразвернутом состоянии.)
using Microsoft.Web.Administration; // Assembly is found in %windir%\System32\inetsrv\Microsoft.Web.Administration.dll
ServerManager iis = new ServerManager();
if (stopIIS) iis.Sites[site].Stop(); // bool stopIIS and string site are set by command line option (and have hardcoded defaults).
string[] files = Directory.GetFiles(workDir, "*");
foreach (string file in files)
{
string name = file.Substring(file.LastIndexOf("\\") + 1);
if (name == "web.config") continue; // The web.config for production is different from that used in development and kept in svn.
try
{
File.Delete(dest + name); // string dest is a command line option (and has a hard-coded default).
}
catch (Exception ex) { }
File.Move(file, dest + name);
}
string[] dirs = Directory.GetDirectories(workDir);
foreach (string dir in dirs)
{
string name = dir.Substring(dir.LastIndexOf("\\") + 1);
if (name == "dyn") continue; // A folder I want to ignore.
try
{
Directory.Delete(dest + name, true);
}
catch (DirectoryNotFoundException ex) { }
Directory.Move(dir, dest + name);
}
if (stopIIS) iis.Sites[site].Start();
И мы закончили. Уф!
Есть несколько деталей, которые я пропустил в приведенном выше коде - например, я удаляю все файлы * .psd в моей папке с изображениями и записываю сообщение об авторских правах в мой скомпилированный файл js - но заполнение пробелов - это наполовину удовольствие , верно!?
Очевидно, что часть кода, который я представил здесь, применима только к конкретным проектным решениям, которые я принял для своего сайта, но я надеюсь, что некоторые из вас найдут [части] этого полезными, если вы решите создать автоматизированный процесс развертывания - который, для протокола, я настоятельно рекомендую. Очень приятно иметь возможность зафиксировать мои изменения в svn, ssh на моем производственном сервере, запустить это и все готово.