当前能百度到的webpack打包包含oclazyload组件的angular1.x工程的文章大多用的是webpack1.x版本,但实际上webpack.config里很多1.x版本的配置项在最新的4.6.0里已经废弃了(或名称、位置发生了调整),这意味着我们直接拿用webpack1.x版本可以成功打包的源码工程过来用最新版本的webpack打包是会报错的,出错信息可以看这里了解一下:webpack打包时提示Invalid configuration object错误
下面介绍一下在webpack4.6.0版本下如何打包包含oclazyload组件的angular1.x工程。
首先给出笔者的一些环境信息:
node v8.10.0
npm 5.6.0
webpack 4.6.0
angular 1.4.6
angular-ui-router 0.2.18
oclazyload 1.0.1
接下来来简单分析一下如果要用webpack来打包工程要做哪些调整:
webpack的基础功能就是把工程里多个有依赖关系的javascript文件打包压缩成一个文件,但使用了oclazyload组件即意味着工程里的javascript文件并不是同时加载的(不是同时加载的文件肯定不能打包压缩在同一个文件中),这意味着webpack也要支持懒加载,好在webpack早就支持了,这个懒加载功能可以通过require.ensure来完成,接口文档可以看这里:require.ensure接口
ps:有个坏消息,笔者写这篇博客的时候webpack的接口文档貌似已经更新到4.8.1了,请各位同学注意(没错,接口可能又变了)。
首先看一下在不使用webpack打包时懒加载是怎么玩的:
$stateProvider.state("default",{
url:"/default",
//本地直接在浏览器中运行index.html则不能使用templateUrl,否则会有跨域问题
// templateUrl:"html/default.html",
template: "<div><span>{{default}}</span></div>",
controller: "defaultCtrl",
resolve:{
'default': function($ocLazyLoad) {
let mod = require('./controller/defaultCtrl');
return $ocLazyLoad.load({
name: mod
});
}
}
});
直接把需要懒加载的文件load进来就可以了(有兴趣的同学戳这里看基本用法:angularjs1.4懒加载的基本用法)。
然后按正常人的脑回路来说一般会尝试这么写:
$stateProvider.state("default",{
url:"/default",
//本地直接在浏览器中运行index.html则不能使用templateUrl,否则会有跨域问题
// templateUrl:"html/default.html",
template: "<div><span>{{default}}</span></div>",
controller: "defaultCtrl",
resolve:{
'default': ["$ocLazyLoad", function($ocLazyLoad) {
require.ensure(['./controller/defaultCtrl'], function() {
let mod = require('./controller/defaultCtrl');
return $ocLazyLoad.load({
name: mod
});
}, 'defaultCtrl');
}]
}
});
不过按一般的套路来说这么容易就搞定的话笔者就没什么好写的了,实际上按上面的写法运行时会提示路由所需的controller不存在,并抛出异常终止程序运行:
推测是require.ensure内部有异步行为,resolve对应的回调函数在执行完成时require.ensure的回调函数其实还没有执行,下面尝试用angular的promise把require.ensure包起来:
$stateProvider.state("default",{
url:"/default",
//本地直接在浏览器中运行index.html则不能使用templateUrl,否则会有跨域问题
// templateUrl:"html/default.html",
template: "<div><span>{{default}}</span></div>",
controller: "defaultCtrl",
resolve:{
'default': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/defaultCtrl'], function() {
let mod = require('./controller/defaultCtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'defaultCtrl');
return deferred.promise;
}]
}
});
重新用webpack打包后看一下运行结果:
再手动切换一下路由看看其他路由所需的controller是否可以正常加载:
嗯,简单验证了一下基本没什么大问题了。
下面贴一下路由模块的完整代码:
"use strict";
let configModule = angular.module("config", []);
let config = ["$stateProvider", "$urlRouterProvider", "$controllerProvider",
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise("/default");
$stateProvider.state("default",{
url:"/default",
//本地直接在浏览器中运行index.html则不能使用templateUrl,否则会有跨域问题
// templateUrl:"html/default.html",
template: "<div><span>{{default}}</span></div>",
controller: "defaultCtrl",
resolve:{
'default': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/defaultCtrl'], function() {
let mod = require('./controller/defaultCtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'defaultCtrl');
return deferred.promise;
}]
// 'default': ["$ocLazyLoad", function($ocLazyLoad) {
// require.ensure(['./controller/defaultCtrl'], function() {
// let mod = require('./controller/defaultCtrl');
// return $ocLazyLoad.load({
// name: mod
// });
// }, 'defaultCtrl');
// }]
}
});
$stateProvider.state("routerA",{
url:"/routerA",
// templateUrl:"html/routerA.html",
template: "<div>{{say}}</div>`",
controller: "routerACtrl",
resolve:{
'routerA': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/routerACtrl'], function() {
let mod = require('./controller/routerACtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'routerACtrl');
return deferred.promise;
}]
}
});
}];
configModule.config(config);
module.exports = configModule.name;
可以看到有2个路由模块,那么使用webpack打包后应该会生成三个文件:
额外生成的0.bundle.js和1.bundle.js对应的就是懒加载的2个路由对应的controller文件。
下面再贴一下默认路由的controller代码:
"use strict";
let defaultCtrl = ["$rootScope", "$scope", function ($rootScope, $scope) {
console.log("enter default controller");
$scope.default = "default page."
}];
module.exports = angular.module("config", [])
.controller("defaultCtrl", defaultCtrl).name;
另外要注意的一点是export default和module.exports导出的模块require时的写法是不同的(export default如果用import就没问题,但是要想混用require就必须注意写法),详情可以参考在路由的resolve中使用lazyload加载控制器无效这篇文章。
上面的例子已经简单说明该如何打包包含oclazyload组件(懒加载)的angular1.x工程,但深入使用的话还有坑要踩。之前的路由模块代码只给出了一层路由的情况,一旦配置成多层路由就会有下面这样的问题:
包含多层路由的路由模块代码:
"use strict";
let configModule = angular.module("config", []);
let config = ["$stateProvider", "$urlRouterProvider", "$controllerProvider",
function($stateProvider, $urlRouterProvider){
// $urlRouterProvider.otherwise("/default/subPage");
$urlRouterProvider.otherwise("/default");
$stateProvider.state("default",{
url:"/default",
//本地直接在浏览器中运行index.html则不能使用templateUrl,否则会有跨域问题
// templateUrl:"html/default.html",
template: "<div><span>{{default}}</span><div ui-view></div></div>",
controller: "defaultCtrl",
resolve:{
'default': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/defaultCtrl'], function() {
let mod = require('./controller/defaultCtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'defaultCtrl');
return deferred.promise;
}]
// 'default': ["$ocLazyLoad", function($ocLazyLoad) {
// require.ensure(['./controller/defaultCtrl'], function() {
// let mod = require('./controller/defaultCtrl');
// return $ocLazyLoad.load({
// name: mod
// });
// }, 'defaultCtrl');
// }]
}
});
$stateProvider.state("default.subPage",{
url:"/subPage",
// templateUrl:"html/routerA.html",
template: "<div>{{subPageData}}</div>`",
controller: "subPageCtrl",
resolve:{
'subPageCtrl': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/subPageCtrl'], function() {
let mod = require('./controller/subPageCtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'subPageCtrl');
return deferred.promise;
}]
}
});
$stateProvider.state("routerA",{
url:"/routerA",
// templateUrl:"html/routerA.html",
template: "<div>{{say}}</div>`",
controller: "routerACtrl",
resolve:{
'routerA': ["$q", "$ocLazyLoad", function($q, $ocLazyLoad) {
let deferred = $q.defer();
require.ensure(['./controller/routerACtrl'], function() {
let mod = require('./controller/routerACtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'routerACtrl');
return deferred.promise;
}]
}
});
}];
configModule.config(config);
module.exports = configModule.name;
subPage路由的controller代码:
"use strict";
let subPageCtrl = ["$rootScope", "$scope", function ($rootScope, $scope) {
console.log("enter subPage controller");
$scope.subPageData = "subPage page."
}];
module.exports = angular.module("config", [])
.controller("subPageCtrl", subPageCtrl).name;
执行webpack后访问/default/subPage路由:
笔者的开发代码(不是github里给出的代码)里这似乎是个偶现问题,不是每次都会报错,且提示未定义的controller有可能是父路由也有可能是子路由。
导致这个问题发生的根因是路由模块路由controller的加载方式及路由controller的定义方式。
首先来看一下路由controller的定义方式:
default路由controller:
"use strict";
let defaultCtrl = ["$rootScope", "$scope", function ($rootScope, $scope) {
console.log("enter default controller");
$scope.default = "default page."
}];
module.exports = angular.module("config", [])
.controller("defaultCtrl", defaultCtrl).name;
subPage路由controller:
"use strict";
let subPageCtrl = ["$rootScope", "$scope", function ($rootScope, $scope) {
console.log("enter subPage controller");
$scope.subPageData = "subPage page."
}];
module.exports = angular.module("config", [])
.controller("subPageCtrl", subPageCtrl).name;
父子路由的controller都定义在config这个module上,再看一下路由模块路由controller的加载方式(这里只截取了路由模块default路由的片段):
let deferred = $q.defer();
require.ensure(['./controller/defaultCtrl'], function() {
let mod = require('./controller/defaultCtrl');
$ocLazyLoad.load({
name: mod
});
deferred.resolve();
}, 'defaultCtrl');
可以看到ocLazyLoad.load的是export出来的module名称,如果打印出来的话可以看到父子路由export出来的module名称都是config,而ocLazyLoad.load以name的方式来完成module的懒加载属于覆盖式加载,即意味着我们最终load进来的config这个module要么少了defaultCtrl,要么少了subPageCtrl(因为defaultCtrl这个文件export出来的config module没有subPageCtrl,subPageCtrl这个文件export出来的config module没有defaultCtrl),这样报错就不难理解了。
问题改起来也很简单,定义的controller不挂在config这个module上就没问题了,以default路由controller为例(当然所有的controller都要调整):
"use strict";
let defaultCtrl = ["$rootScope", "$scope", function ($rootScope, $scope) {
console.log("enter default controller");
$scope.default = "default page."
}];
module.exports = angular.module("defaultCtrl", [])
.controller("defaultCtrl", defaultCtrl).name;
把module的名称调整为defaultCtrl(其实随便什么名字都行,只要确保module名称不重复即可),然后执行webpack后访问/default/subPage路由(再说一遍,其他controller挂的module名称都要调整):
这样即使刷新再多次也不会报错了。也许有同学有疑问:为什么controller挂的这些module都没有被config或app这两个module声明依赖,但实际上确可以使用这些module上挂的controller?
答案很明显,ocLazyLoad.load以name的方式来完成module的懒加载默认是作为执行ocLazyLoad.load所在module(在这里是config)的依赖项load进来,所以直接使用完全没问题。同理,在这些controller也可以随便使用在config或app上定义的service、factory甚至是directive。
完整工程代码:https://github.com/liqing-taoyanzoukaila/angular-lazyload-webpack。