Jx.Cms開發筆記(六)-重寫Compiler

jvx發表於2022-04-20

我們在Jx.Cms開發筆記(三)-Views主題動態切換中說了如何切換主題。但是這裡有一個問題,就是主題切換時,會報錯

theme.png

這是由於asp.net core在處理Views的資訊的時候是在建構函式中處理的,沒有任何方法可以重新整理這個處理結果。

這裡放最新版的DefaultViewCompiler程式碼,在Jx.Cms編寫的時候程式碼有少許區別,但是基本邏輯是一樣的。

public DefaultViewCompiler(
        ApplicationPartManager applicationPartManager,
        ILogger<DefaultViewCompiler> logger)
    {
        _applicationPartManager = applicationPartManager;
        _logger = logger;
        _normalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);

        EnsureCompiledViews(logger);
    }

    [MemberNotNull(nameof(_compiledViews))]
    private void EnsureCompiledViews(ILogger logger)
    {
        if (_compiledViews is not null)
        {
            return;
        }

        var viewsFeature = new ViewsFeature();
        _applicationPartManager.PopulateFeature(viewsFeature);

        // We need to validate that the all compiled views are unique by path (case-insensitive).
        // We do this because there's no good way to canonicalize paths on windows, and it will create
        // problems when deploying to linux. Rather than deal with these issues, we just don't support
        // views that differ only by case.
        var compiledViews = new Dictionary<string, Task<CompiledViewDescriptor>>(
            viewsFeature.ViewDescriptors.Count,
            StringComparer.OrdinalIgnoreCase);

        foreach (var compiledView in viewsFeature.ViewDescriptors)
        {
            logger.ViewCompilerLocatedCompiledView(compiledView.RelativePath);

            if (!compiledViews.ContainsKey(compiledView.RelativePath))
            {
                // View ordering has precedence semantics, a view with a higher precedence was not
                // already added to the list.
                compiledViews.TryAdd(compiledView.RelativePath, Task.FromResult(compiledView));
            }
        }

        if (compiledViews.Count == 0)
        {
            logger.ViewCompilerNoCompiledViewsFound();
        }

        // Safe races should be ok. We would end up logging multiple times
        // if this is invoked concurrently, but since this is primarily a dev-scenario, we don't think
        // this will happen often. We could always re-consider the logging if we get feedback.
        _compiledViews = compiledViews;
    }

所以程式只能獲取到第一次的_compiledViews,切換後的Views由於沒有放在_compiledViews中,所以無法被找到,就出現了第一圖的那種錯誤。

這裡的解決方法很簡單,我們只需要重寫一個自己的ViewCompiler就可以了,由於官方原始碼全部都是internal的,所以我們只能把這部分內容全部重寫。

我們建立自己的MyViewCompilerProviderMyViewCompiler。由於.Net6在這裡有部分修改,我們的程式是在.Net5時編寫的,所以這裡我們的原始碼是.Net5修改的,與目前最新的程式碼有些許差距,但是不影響正常使用.

MyViewCompiler:

public class MyViewCompiler : IViewCompiler
    {
        private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;
        private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
        private readonly ILogger _logger;

        public MyViewCompiler(
            IList<CompiledViewDescriptor> compiledViews,
            ILogger logger)
        {
            if (compiledViews == null)
            {
                throw new ArgumentNullException(nameof(compiledViews));
            }

            if (logger == null)
            {
                throw new ArgumentNullException(nameof(logger));
            }

            _logger = logger;
            _normalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);

            // We need to validate that the all of the precompiled views are unique by path (case-insensitive).
            // We do this because there's no good way to canonicalize paths on windows, and it will create
            // problems when deploying to linux. Rather than deal with these issues, we just don't support
            // views that differ only by case.
            _compiledViews = new Dictionary<string, Task<CompiledViewDescriptor>>(
                compiledViews.Count,
                StringComparer.OrdinalIgnoreCase);

            foreach (var compiledView in compiledViews)
            {

                if (!_compiledViews.ContainsKey(compiledView.RelativePath))
                {
                    // View ordering has precedence semantics, a view with a higher precedence was not
                    // already added to the list.
                    _compiledViews.Add(compiledView.RelativePath, Task.FromResult(compiledView));
                }
            }

            if (_compiledViews.Count == 0)
            {
                
            }
        }

        /// <inheritdoc />
        public Task<CompiledViewDescriptor> CompileAsync(string relativePath)
        {
            if (relativePath == null)
            {
                throw new ArgumentNullException(nameof(relativePath));
            }

            // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already
            // normalized and a cache entry exists.
            if (_compiledViews.TryGetValue(relativePath, out var cachedResult))
            {
                
                return cachedResult;
            }

            var normalizedPath = GetNormalizedPath(relativePath);
            if (_compiledViews.TryGetValue(normalizedPath, out cachedResult))
            {
                
                return cachedResult;
            }

            // Entry does not exist. Attempt to create one.
            
            return Task.FromResult(new CompiledViewDescriptor
            {
                RelativePath = normalizedPath,
                ExpirationTokens = Array.Empty<IChangeToken>(),
            });
        }

        private string GetNormalizedPath(string relativePath)
        {
            Debug.Assert(relativePath != null);
            if (relativePath.Length == 0)
            {
                return relativePath;
            }

            if (!_normalizedPathCache.TryGetValue(relativePath, out var normalizedPath))
            {
                normalizedPath = NormalizePath(relativePath);
                _normalizedPathCache[relativePath] = normalizedPath;
            }

            return normalizedPath;
        }

        public static string NormalizePath(string path)
        {
            var addLeadingSlash = path[0] != '\\' && path[0] != '/';
            var transformSlashes = path.IndexOf('\\') != -1;

            if (!addLeadingSlash && !transformSlashes)
            {
                return path;
            }

            var length = path.Length;
            if (addLeadingSlash)
            {
                length++;
            }

            return string.Create(length, (path, addLeadingSlash), (span, tuple) =>
            {
                var (pathValue, addLeadingSlashValue) = tuple;
                var spanIndex = 0;

                if (addLeadingSlashValue)
                {
                    span[spanIndex++] = '/';
                }

                foreach (var ch in pathValue)
                {
                    span[spanIndex++] = ch == '\\' ? '/' : ch;
                }
            });
        }
    }

這個類完全複製了.Net5的原始碼,只是刪除了部分編譯不過去的日誌內容。

MyViewCompilerProvider:

public class MyViewCompilerProvider : IViewCompilerProvider
    {
        private MyViewCompiler _compiler;
        private readonly ApplicationPartManager _applicationPartManager;
        private readonly ILoggerFactory _loggerFactory;

        public MyViewCompilerProvider(
            ApplicationPartManager applicationPartManager,
            ILoggerFactory loggerFactory)
        {
            _applicationPartManager = applicationPartManager;
            _loggerFactory = loggerFactory;
            Modify();
        }

        public void Modify()
        {
            var feature = new ViewsFeature();
            _applicationPartManager.PopulateFeature(feature);

            _compiler = new MyViewCompiler(feature.ViewDescriptors, _loggerFactory.CreateLogger<MyViewCompiler>());
        }

        public IViewCompiler GetCompiler() => _compiler;
    }

這個類我們只是把.Net5原始碼裡的建構函式拆分了,拆出了一個PublicModify方法。

然後我們需要用自己的MyViewCompilerProvider替換自帶的,所以我們需要在Startup.csConfigureServices方法中新增services.Replace<IViewCompilerProvider, MyViewCompilerProvider>();

最後我們只需要在需要重新獲取所有Views的地方呼叫viewCompilerProvider?.Modify();即可。

相關文章