NgUpgrade:升级 Angular1 组件时无法使用 templateUrl
NgUpgrade: Unable to use templateUrl when upgrading Angular1 components
我想升级 ng1 组件以在 ng2 组件中使用。
如果我只使用要升级的 ng1 组件的模板字符串,它就可以工作。但是,如果我改用 templateUrl,应用程序会崩溃并给我这个错误:
angular.js:13920 Error: loading directive templates asynchronously is not supported
at RemoteUrlComponent.UpgradeComponent.compileTemplate (upgrade-static.umd.js:720)
at RemoteUrlComponent.UpgradeComponent (upgrade-static.umd.js:521)
at new RemoteUrlComponent (remote-url.component.ts:11)
at new Wrapper_RemoteUrlComponent (wrapper.ngfactory.js:7)
at View_AppComponent1.createInternal (component.ngfactory.js:73)
at View_AppComponent1.AppView.create (core.umd.js:12262)
at TemplateRef_.createEmbeddedView (core.umd.js:9320)
at ViewContainerRef_.createEmbeddedView (core.umd.js:9552)
at eval (common.umd.js:1670)
at DefaultIterableDiffer.forEachOperation (core.umd.js:4653)
这是一个演示我的问题的插件:
https://plnkr.co/edit/2fXvfc?p=info
我已经按照 Angular 1 -> 2 升级指南进行操作,看来这段代码应该有效。我不太确定为什么它不起作用。
我找到了一个非常便宜的解决方案。
只需使用 template: require('./remote-url.component.html')
而不是 templateUrl: './remote-url.component.html'
,它应该可以正常工作!
这真是令人沮丧,因为 Angular 升级文档明确表示可以使用 templateUrl。从不提及这个异步问题。我已经通过使用 $templateCache 找到了绕过它的方法。我不想更改我的 angular 1 指令,因为我的 angular 1 个应用程序使用了它,angular 4 个应用程序也将使用它。所以我必须找到一种方法来即时修改它。我使用了 $delegate、$provider 和 $templateCache。我的代码如下。我还使用它来删除 replace 属性,因为它已被弃用。
function upgradeDirective(moduleName, invokedName) {
/** get the invoked directive */
angular.module(moduleName).config(config);
config.$inject = ['$provide'];
decorator.$inject = ['$delegate', '$templateCache'];
function config($provide) {
$provide.decorator(invokedName + 'Directive', decorator);
}
function decorator($delegate, $templateCache) {
/** get the directive reference */
var directive = $delegate[0];
/** remove deprecated attributes */
if (directive.hasOwnProperty('replace')){
delete directive.replace;
}
/** check for templateUrl and get template from cache */
if (directive.hasOwnProperty('templateUrl')){
/** get the template key */
var key = directive.templateUrl.substring(directive.templateUrl.indexOf('app/'));
/** remove templateUrl */
delete directive.templateUrl;
/** add template and get from cache */
directive.template = $templateCache.get(key);
}
/** return the delegate */
return $delegate;
}
}
upgradeDirective('moduleName', 'moduleDirectiveName');
解决这个问题的一个技术含量很低的解决方案是在 index.html 中加载模板,并为它们分配与指令正在寻找的 templateUrls 相匹配的 ID,即:
<script type="text/ng-template" id="some/file/path.html">
<div>
<p>Here's my template!</p>
</div>
</script>
Angular 然后自动将模板放入 $templateCache 中,这是 UpgradeComponent 的 compileTemplate 寻找模板开始的地方,因此无需更改指令中的 templateUrl,事情就会起作用,因为 id 匹配模板网址。
如果您查看 UpgradeComponent 的源代码(见下文),您可以看到处理获取 url 的注释掉的代码,所以它一定在工作中,但暂时这可能是一个可行的解决方案,甚至是一个可编写脚本的解决方案。
private compileTemplate(directive: angular.IDirective): angular.ILinkFn {
if (this.directive.template !== undefined) {
return this.compileHtml(getOrCall(this.directive.template));
} else if (this.directive.templateUrl) {
const url = getOrCall(this.directive.templateUrl);
const html = this.$templateCache.get(url) as string;
if (html !== undefined) {
return this.compileHtml(html);
} else {
throw new Error('loading directive templates asynchronously is not supported');
// return new Promise((resolve, reject) => {
// this.$httpBackend('GET', url, null, (status: number, response: string) => {
// if (status == 200) {
// resolve(this.compileHtml(this.$templateCache.put(url, response)));
// } else {
// reject(`GET component template from '${url}' returned '${status}: ${response}'`);
// }
// });
// });
}
} else {
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
}
}
我已经创建了一个方法实用程序来解决这个问题。
基本上它将模板 url 的内容添加到 angular 的模板缓存中,
使用 requireJS 和 "text.js":
initTemplateUrls(templateUrlList) {
app.run(function ($templateCache) {
templateUrlList.forEach(templateUrl => {
if ($templateCache.get(templateUrl) === undefined) {
$templateCache.put(templateUrl, 'temporaryValue');
require(['text!' + templateUrl],
function (templateContent) {
$templateCache.put(templateUrl, templateContent);
}
);
}
});
});
你应该做的是把这个方法实用程序放在 appmodule.ts 中,然后创建一个你将要从你的 angular 指令升级的 templateUrls 列表,例如:
const templateUrlList = [
'/app/@fingerprint@/common/directives/grid/pGrid.html',
];
作为一种变通方法,我使用 $templateCache 和 $templateRequest 将模板放入 $templateCache 中以获得 Angular 需要的模板,在 AngularJS 运行 上,如下所示:
app.run(['$templateCache', '$templateRequest', function($templateCache, $templateRequest) {
var templateUrlList = [
'app/modules/common/header.html',
...
];
templateUrlList.forEach(function (templateUrl) {
if ($templateCache.get(templateUrl) === undefined) {
$templateRequest(templateUrl)
.then(function (templateContent) {
$templateCache.put(templateUrl, templateContent);
});
}
});
}]);
在尝试使用 requireJS 和对我不起作用的文本插件进行 require 之后,我设法使用 'ng-include' 使其工作,如下所示:
angular.module('appName').component('nameComponent', {
template: `<ng-include src="'path_to_file/file-name.html'"></ng-include>`,
希望对您有所帮助!
我为此使用 webpack 的 require.context:
模板-factory.js
import {resolve} from 'path';
/**
* Wrap given context in AngularJS $templateCache
* @param ctx - A context module
* @param dir - module directory
* @returns {function(...*): void} - AngularJS Run function
*/
export const templatesFactory = (ctx, dir, filename) => {
return $templateCache => ctx.keys().forEach(key => {
const templateId = (() => {
switch (typeof filename) {
case 'function':
return resolve(dir, filename(key));
case 'string':
return resolve(dir, filename);
default:
return resolve(dir, key);
}
})();
$templateCache.put(templateId, ctx(key));
});
};
app.html-bundle.js
import {templatesFactory} from './templates-factory';
const ctx = require.context('./', true, /\.html$/);
export const AppHtmlBundle = angular.module('AppHtmlBundle', [])
.run(templatesFactory(ctx, __dirname))
.name;
不要忘记将 html-loader 添加到您的 webpack.config.js:
[{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
minimize: false,
root: path.resolve(__dirname, './src')
}
}
}]
您可能还需要将相对路径转换为绝对路径。为此,我使用自己编写的 babel 插件 ng-template-url-absolutify:
[{
test: /\.(es6|js)$/,
include: [path.resolve(__dirname, 'src')],
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-syntax-dynamic-import',
['ng-template-url-absolutify', {baseDir: path.resolve(__dirname, 'src'), baseUrl: ''}]
],
presets: [['@babel/preset-env', {'modules': false}]]
}
},
这里给出的大部分答案都涉及以某种方式预加载模板,以便使其对指令同步可用。
如果您想避免这样做 - 例如如果你有一个包含许多模板的大型 AngularJS 应用程序,并且你不想预先下载它们,你可以简单地将指令包装在一个同步加载的版本中。
例如,如果您有一个名为 myDirective
的指令,它有一个您不想预先下载的异步加载的 templateUrl
,您可以这样做:
angular
.module('my-module')
.directive('myDirectiveWrapper', function() {
return {
restrict: 'E',
template: "<my-directive></my-directive>",
}
});
然后你升级的 Angular 指令只需要提供 'myDirectiveWrapper'
而不是 'myDirective'
在它的 super()
调用扩展 UpgradeComponent
.
如果您不想修改您的 Webpack 配置,quick/dirty 解决方案是使用 raw-loader 导入语法:
template: require('!raw-loader!./your-template.html')
我想升级 ng1 组件以在 ng2 组件中使用。
如果我只使用要升级的 ng1 组件的模板字符串,它就可以工作。但是,如果我改用 templateUrl,应用程序会崩溃并给我这个错误:
angular.js:13920 Error: loading directive templates asynchronously is not supported
at RemoteUrlComponent.UpgradeComponent.compileTemplate (upgrade-static.umd.js:720)
at RemoteUrlComponent.UpgradeComponent (upgrade-static.umd.js:521)
at new RemoteUrlComponent (remote-url.component.ts:11)
at new Wrapper_RemoteUrlComponent (wrapper.ngfactory.js:7)
at View_AppComponent1.createInternal (component.ngfactory.js:73)
at View_AppComponent1.AppView.create (core.umd.js:12262)
at TemplateRef_.createEmbeddedView (core.umd.js:9320)
at ViewContainerRef_.createEmbeddedView (core.umd.js:9552)
at eval (common.umd.js:1670)
at DefaultIterableDiffer.forEachOperation (core.umd.js:4653)
这是一个演示我的问题的插件:
https://plnkr.co/edit/2fXvfc?p=info
我已经按照 Angular 1 -> 2 升级指南进行操作,看来这段代码应该有效。我不太确定为什么它不起作用。
我找到了一个非常便宜的解决方案。
只需使用 template: require('./remote-url.component.html')
而不是 templateUrl: './remote-url.component.html'
,它应该可以正常工作!
这真是令人沮丧,因为 Angular 升级文档明确表示可以使用 templateUrl。从不提及这个异步问题。我已经通过使用 $templateCache 找到了绕过它的方法。我不想更改我的 angular 1 指令,因为我的 angular 1 个应用程序使用了它,angular 4 个应用程序也将使用它。所以我必须找到一种方法来即时修改它。我使用了 $delegate、$provider 和 $templateCache。我的代码如下。我还使用它来删除 replace 属性,因为它已被弃用。
function upgradeDirective(moduleName, invokedName) {
/** get the invoked directive */
angular.module(moduleName).config(config);
config.$inject = ['$provide'];
decorator.$inject = ['$delegate', '$templateCache'];
function config($provide) {
$provide.decorator(invokedName + 'Directive', decorator);
}
function decorator($delegate, $templateCache) {
/** get the directive reference */
var directive = $delegate[0];
/** remove deprecated attributes */
if (directive.hasOwnProperty('replace')){
delete directive.replace;
}
/** check for templateUrl and get template from cache */
if (directive.hasOwnProperty('templateUrl')){
/** get the template key */
var key = directive.templateUrl.substring(directive.templateUrl.indexOf('app/'));
/** remove templateUrl */
delete directive.templateUrl;
/** add template and get from cache */
directive.template = $templateCache.get(key);
}
/** return the delegate */
return $delegate;
}
}
upgradeDirective('moduleName', 'moduleDirectiveName');
解决这个问题的一个技术含量很低的解决方案是在 index.html 中加载模板,并为它们分配与指令正在寻找的 templateUrls 相匹配的 ID,即:
<script type="text/ng-template" id="some/file/path.html">
<div>
<p>Here's my template!</p>
</div>
</script>
Angular 然后自动将模板放入 $templateCache 中,这是 UpgradeComponent 的 compileTemplate 寻找模板开始的地方,因此无需更改指令中的 templateUrl,事情就会起作用,因为 id 匹配模板网址。
如果您查看 UpgradeComponent 的源代码(见下文),您可以看到处理获取 url 的注释掉的代码,所以它一定在工作中,但暂时这可能是一个可行的解决方案,甚至是一个可编写脚本的解决方案。
private compileTemplate(directive: angular.IDirective): angular.ILinkFn {
if (this.directive.template !== undefined) {
return this.compileHtml(getOrCall(this.directive.template));
} else if (this.directive.templateUrl) {
const url = getOrCall(this.directive.templateUrl);
const html = this.$templateCache.get(url) as string;
if (html !== undefined) {
return this.compileHtml(html);
} else {
throw new Error('loading directive templates asynchronously is not supported');
// return new Promise((resolve, reject) => {
// this.$httpBackend('GET', url, null, (status: number, response: string) => {
// if (status == 200) {
// resolve(this.compileHtml(this.$templateCache.put(url, response)));
// } else {
// reject(`GET component template from '${url}' returned '${status}: ${response}'`);
// }
// });
// });
}
} else {
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
}
}
我已经创建了一个方法实用程序来解决这个问题。 基本上它将模板 url 的内容添加到 angular 的模板缓存中, 使用 requireJS 和 "text.js":
initTemplateUrls(templateUrlList) {
app.run(function ($templateCache) {
templateUrlList.forEach(templateUrl => {
if ($templateCache.get(templateUrl) === undefined) {
$templateCache.put(templateUrl, 'temporaryValue');
require(['text!' + templateUrl],
function (templateContent) {
$templateCache.put(templateUrl, templateContent);
}
);
}
});
});
你应该做的是把这个方法实用程序放在 appmodule.ts 中,然后创建一个你将要从你的 angular 指令升级的 templateUrls 列表,例如:
const templateUrlList = [
'/app/@fingerprint@/common/directives/grid/pGrid.html',
];
作为一种变通方法,我使用 $templateCache 和 $templateRequest 将模板放入 $templateCache 中以获得 Angular 需要的模板,在 AngularJS 运行 上,如下所示:
app.run(['$templateCache', '$templateRequest', function($templateCache, $templateRequest) {
var templateUrlList = [
'app/modules/common/header.html',
...
];
templateUrlList.forEach(function (templateUrl) {
if ($templateCache.get(templateUrl) === undefined) {
$templateRequest(templateUrl)
.then(function (templateContent) {
$templateCache.put(templateUrl, templateContent);
});
}
});
}]);
在尝试使用 requireJS 和对我不起作用的文本插件进行 require 之后,我设法使用 'ng-include' 使其工作,如下所示:
angular.module('appName').component('nameComponent', {
template: `<ng-include src="'path_to_file/file-name.html'"></ng-include>`,
希望对您有所帮助!
我为此使用 webpack 的 require.context:
模板-factory.js
import {resolve} from 'path';
/**
* Wrap given context in AngularJS $templateCache
* @param ctx - A context module
* @param dir - module directory
* @returns {function(...*): void} - AngularJS Run function
*/
export const templatesFactory = (ctx, dir, filename) => {
return $templateCache => ctx.keys().forEach(key => {
const templateId = (() => {
switch (typeof filename) {
case 'function':
return resolve(dir, filename(key));
case 'string':
return resolve(dir, filename);
default:
return resolve(dir, key);
}
})();
$templateCache.put(templateId, ctx(key));
});
};
app.html-bundle.js
import {templatesFactory} from './templates-factory';
const ctx = require.context('./', true, /\.html$/);
export const AppHtmlBundle = angular.module('AppHtmlBundle', [])
.run(templatesFactory(ctx, __dirname))
.name;
不要忘记将 html-loader 添加到您的 webpack.config.js:
[{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
minimize: false,
root: path.resolve(__dirname, './src')
}
}
}]
您可能还需要将相对路径转换为绝对路径。为此,我使用自己编写的 babel 插件 ng-template-url-absolutify:
[{
test: /\.(es6|js)$/,
include: [path.resolve(__dirname, 'src')],
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-syntax-dynamic-import',
['ng-template-url-absolutify', {baseDir: path.resolve(__dirname, 'src'), baseUrl: ''}]
],
presets: [['@babel/preset-env', {'modules': false}]]
}
},
这里给出的大部分答案都涉及以某种方式预加载模板,以便使其对指令同步可用。
如果您想避免这样做 - 例如如果你有一个包含许多模板的大型 AngularJS 应用程序,并且你不想预先下载它们,你可以简单地将指令包装在一个同步加载的版本中。
例如,如果您有一个名为 myDirective
的指令,它有一个您不想预先下载的异步加载的 templateUrl
,您可以这样做:
angular
.module('my-module')
.directive('myDirectiveWrapper', function() {
return {
restrict: 'E',
template: "<my-directive></my-directive>",
}
});
然后你升级的 Angular 指令只需要提供 'myDirectiveWrapper'
而不是 'myDirective'
在它的 super()
调用扩展 UpgradeComponent
.
如果您不想修改您的 Webpack 配置,quick/dirty 解决方案是使用 raw-loader 导入语法:
template: require('!raw-loader!./your-template.html')