December 14, 2017

angularJs 配合ui-router,webpack, ocLazyLoad懒加载

前言

自从工作之后就极少写文章了,因为空闲时间没这么多,到了周末又想轻松两天,但是周末其实并不轻松,或许归根到底最后就是一个字:!这周回家之后,感觉总算可以静下心来做点东西,把之前在项目用ng1.x按需加载的实现整理一下。

需求背景

最近工作用到angularJs,也就是ng1.x版本开发一个网站,这个项目其中用ui-router来控制路由,webpack来构建项目。有个比较致命的痛点,ng1.x官方不支持懒加载!

这个项目经过webpack打包之后主要形成两个js文件,一个是vendor.js,是引入的node_modules的公用文件,另外一个是app.js,是自己写的js文件。

处理之前项目的js入口文件大概是这样子的;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 引入主要工具和框架
import angular from './angular'
import 'angular-ui-router'

// 引入一些工具库
import tool1 from './tool1'

// 引入ng的一些指令,组件,service等等
import aComponent from './a.component'
import aService from './a.service'
import aDirective from './a.directive'

// 引入路由
import router from './router'

// 项目模块
angular.module('app', [
    'ui.router',
    // ... 其他一些依赖
])
.config(router)
.component('aComponent', aComponent)
// ...

从这个入口文件就可以看出,现在所有依赖的外部工具库和自己编写的内容都是一次性引入进来,尽管通过webpack来分开了两个文件,但是在入口的html文件还是一次引入了。特别是进入首页介绍页面的时候,逻辑功能比较少,但是却要加载全部功能。

解决思路

路由分模块定义

因为项目通过一级url分工明显,每个url可以分离成一个模块,处理起来就更直观,例如:

1
2
3
4
/foo
/bar
/baz
// ...

因此需要从ui-router先下手,能够指定一级url之后,交给对应的模块处理,对应的模块内部再处理子url,这样子每个模块之间就更加明确。从ui-router官网参考的例子如下:

1
2
3
4
5
6
7
var contactsFutureState = {
  name: 'contacts.**',
  url: '/contacts',
  lazyLoad: function() {
    // lazy load the contacts module here
  }
}

这里的例子大概意思是url为/contact的命名是contact.**,然后通过layLoad的函数加载对应的模块逻辑处理。这里要引入一个概念,叫futureState,字面上的意思是未来的状态,就是预先定义的。在配置页面全局路由的时候,大概就是这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular.module('app', ['ui-router'])
.config(['$stateProvider', function ($stateProvider) {
   let states = [
        {
            name: 'foo.**',
            url: '/foo',
            lazyLoad: function() {
                // 引入对应的模块
            }
        },
        {
            name: 'bar.**',
            url: '/bar',
            lazyLoad: function() {
                // 引入对应的模块
            }
        }
        // ...
    ]
    // 定义相关url
    states.forEach(state => $stateProvider.state(state))
}])

然后具体foo模块就定义对应的二级url:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular.module('foo', ['ui-router'])
.config(['$stateProvider', function ($stateProvider) {
   let states = [
        {
            name: 'foo',
            url: '/foo',
            component: 'foo'
        },
        {
            name: 'foo.second',
            // 实际访问的url是 /foo/second
            url: '/second',
            component: 'fooSecond'
        }
        // ...
    ]
    // 定义相关url
    states.forEach(state => $stateProvider.state(state))
}])
// foo和fooSecond也是在该模块引入,这里没有写出
.component('foo', foo)
.component('fooSecond', fooSecond)

但是,这样子并跑不通,会报一个multiple define的多重定义的错,然而ui-router官方并没有给出相关例子,刚才的foo模块定义是根据之前定义全局模块的定义。直到后来在stackflow找到了ui-router注入对象$stateRegistry,替换子模块的$stateProvider,因此子模块定义url的时候,就变成了:

1
2
// 注册相关url,依赖的注入也要修改
states.forEach(state => $stateRegistry.state(state))

动态注入

路由处理完毕之后,就要考虑一下怎么把刚才的子模块在对应路由触发的时候,动态注入,这个在ui-router给出了一个参考,就是利用第三方的ocLazyLoad来支撑,在定义全局路由的时候,表明懒加载,例如:

1
2
3
4
5
6
7
8
[{
    name: 'foo.**',
    url: '/foo',
    lazyLoad: function ($transition$) {
        return $transition$.injector().get('$ocLazyLoad').load('./fooModule.js');
    }
    }
}]

分离代码

动态注入ok了,然后就是用webpack工具来打包分离代码,分离比较简单,教程在这里,有使用importrequire.ensure的方法,这里就使用了import的方法,修改lazyLoad的动态注入方法:

1
2
3
4
5
6
7
8
9
10
11
12
[{
    name: 'foo.**',
    url: '/foo',
    lazyLoad: function ($transition$) {
        return import(/* webpackChunkName: "foo" */ './fooModule.js')
            .then(mod => {
                // mod.defatut 是因为fooModule.js export default ...
                $transition$.injector().get('$ocLazyLoad').load(mod.default)
            })
    }
    }
}]

这样子webpack打包文件的时候会分割代码,当该模块触发的时候,再请求该模块的文件,然后给到ocLazyLoad来动态注入,实现按需加载,当访问过该模块的时候,下次进入已加载过的模块,也不会再次发出请求模块文件

总结

到这里,整个流程就跑通了,回顾一下几个关键点:

  1. 预定义路由future state;
  2. 子模块使用$stateRegistry来注册路由;
  3. 使用ocLazyLoad实现动态注入;
  4. 使用webpack分离代码打包

这次也是在填ng1.x的一些坑,使用相对较旧的框架实现一些看起来比较简单的需求,有时候也是挺折腾的。或许像同事所说的这是旧框架与人民日益增长的需求之间的矛盾

安利

回顾之前用hexo写文章的时候,换了电脑之后,源文件又要重新找,而且过程搭建也是挺麻烦的,所以这次写博客用到同事贡献自动化博客,安利一下地址,仅仅用github的issue就可以写了,而且不怕丢失了,而且一次配置,绝无手尾,写完issue就能更新到我们的博客了。END