SystemJS Multi-Page多页面实践&总结

背景

自从webpack三番五次因为工具链太重在公司内部推不起来也看不到推起来的希望以后,从此绝了在项目中使用webpack的念头,唯一使用了webpack的项目仅剩微信端的一个单人独奏项目。

然后开始寻找webpack的替代品。目标是:

  • 支持UMD规范
  • 有合用的打包的工具并且可以整合gulp
  • 不需要工具链就可以运行起来

然后,我找到了SystemJS这个新玩具,记得当时看到5K+的star,感觉好激动,写本文时候我又去看了下,5355,然后又看到自己居然没有star一下,遂star之,恩,现在是5356。

本文是一个记录,记录自己在项目实践中的诸多总结,总体项目背景如下:传统的多页面应用,不需要兼容旧浏览器,总体页面逻辑是顶部和右侧菜单不变,点击栏目链接不刷新更新中间区域,具体的业务逻辑这里不提,不是重点

SystemJS介绍

写之前简单介绍下SystemJs是什么。
Github上的介绍是:

Universal dynamic module loader
Universal dynamic module loader - loads ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS. Works with both Traceur and Babel.

  • Loads any module format with exact circular reference and binding support.
  • Loads ES6 modules compiled into the System.register bundle format for production, maintaining circular references support.
  • Supports RequireJS-style map, paths, bundles and global shims.
  • Loader plugins allow loading assets through the module naming system such as CSS, JSON or images.

翻译一下:

UMD规范加载器
UMD规范加载器 - 加载ES6、AMD、CommonJS模块和全局脚本,可以运行在浏览器和NodeJS环境,可以与Traceur和Babel一起协作。

  • 各种规范的模块之间的循环引用功能和绑定支持
  • ES6模块在实际生产环境编译为System.register的bundle格式,维持循环引用
  • 支持RequireJS式map, paths, bundles和全局垫片技术(global shims).
  • 支持通过各种模块加载CSS, JSON or images等资源

貌似功能都写的很具体了,但是好像不需要这么多的列表,这里简单说下自己认为的重点,那就是:

各种规范的模块之间的循环引用功能

是的,就一个就足够说服我自己用起来了,尤其是,它不需要工具链支持就可以运行起来!当然,到了产品发布阶段,还是需要打包才能达到性能优化目标。

SystemJS专属包管理工具

jspm官网的介绍感觉有些抓不住重点。

我这里就自己实践,简单说下jspm的功能

  • 初始化SystemJS项目,下载system依赖,生成config.js等配置文件
  • 安装各种npm和github上的包和组件,并自动化配置config.js的map映射,并可以设置自己的类似npm这样的源
  • 打包模块

这里简单说下jspm日常使用:

安装jspm

1
npm i -g jspm

初始化项目

1
jspm init
1
2
3
4
5
6
7
8
Package.json file does not exist, create it? [yes]: 
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [.]:
Enter jspm packages folder [./jspm_packages]:
Enter config file path [./config.js]:
Configuration file config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Which ES6 transpiler would you like to use, Traceur or Babel? [babel]:

下载安装包

1
2
3
4
5
6
#从npm
jspm install npm:jquery
#从github
jsom install github:lodash/lodash
#设置map,这样可以直接require('jq')
jspm install jq=npm:jquery

构建自己的git源

构建自己的git源可以用来处理维护问题,新项目git clone以后直接jspm install一遍就可以直接运行。
这里使用jspm-git来做自己的私有registries。

1
2
3
4
5
#安装
npm install --save-dev jspm-git
#创建
jspm registry create <registryName> jspm-git
jspm registry config <registryName>

这样处理之后,就要输入git的位置了,建议企业项目要用自己的私有git,不要使用公有仓库
处理完毕以后,配置文件位置在 ~/.jspm/config,以后可以按需更改,需要注意的重点是:

  • 私有仓库需要添加ssh key,不然包的这个安装过程是很折腾人的,具体如何添加ssh key,不在本文范围内。
  • 可以设置内网的Git公开库,这样可以在安全和便捷上达到平衡。

SystemJS基本用法

jQuery插件使用:

jQuery插件需要先加载jQuery,然后加载插件,如下

1
2
3
4
5
6
7
8
9
10
System.import('jquery')
.then(function (jQuery) {
window.jQuery = window.$ = jQuery;
return System.import("xdan/datetimepicker")
})
.then(function(){
$('#datetimepicker').datetimepicker({
lang:'zh'
});
})

多依赖处理:

假设a.js依赖b.js和c.js,只有两个加载完成后a.js才能正常运行:

1
2
3
4
5
6
7
8
Promise.all(["b.js","c.js"].map(function(x){        //先加载b.js和c.js
return System.import(x)
})).then(function(m){
var b = m[0],c = m[1]; //为b和c设置别名以便调用
return System.import('a.js') //加载a.js
}).then(function(a){
a();
})

如何编写业务代码:

1
2
3
4
5
6
7
8
9
10
11
//引入jQuery
System.import('jquery')
.then(function($){
window.jQuery = window.$ = $;
//这是业务代码
return System.import("modules/custom/test.js");
})
//app是业务代码的别名
.then(function(app){
app();
})

test.js

1
2
3
4
//module.exports是暴露出来的对接口
module.exports = function(){
$("body").append("just a test");
}

PS:如果你并不想每次都执行一次app(),也不想使用函数式编程,那么test.js是这样的:

1
2
3
module.exports = (function () {
$("body").append("just a test");
})();

但是这里有个地方需要注意,这样的代码仅仅在js加载时候有效果,当js使用缓存时候,js将不被再次执行。
引用时候这样就可以了:

1
2
3
4
5
6
7
//引入jQuery
System.import('jquery')
.then(function($){
window.jQuery = window.$ = $;
//这是业务代码
return System.import("modules/custom/test.js");
})

不改代码,全局暴露

类似这样的一段代码:

1
2
3
test.js
$("body").append("just a test");
alert("test1234");

可以这样引用:

1
2
3
4
5
6
7
//引入jQuery
System.import('jquery')
.then(function($){
window.jQuery = window.$ = $;
//这是业务代码
return System.import("modules/custom/test.js");
})

在JS中引用模块

最简引用:

1
System.import("modules/custom/test.js")

这就够了,但是如果你在JS中用到了jQuey,test.js还是要处理一下:

test.js

1
2
3
4
//引入jQuery
var $ = require("jquery");
$("body").append("just a test");
alert("test1234");

AMD和CMD的循环引用

上面提到的import实际上用到的CMD规范,遇到只支持AMD,并且依赖异常复杂库(如echarts)的如何处理?
SystemJS可以支持AMD和CMD规范,
如果使用CMD规范的模块,需要使用Promise来确保引入顺序,
如果使用AMD规范的话,声明:

1
require=System.amdRequire;

然后JS中这样处理:

1
2
3
4
5
6
7
8
9
10
require(
[
'jquery',
'echarts',
'echarts/src/config',
'echarts/src/chart/line', // 使用柱状图就加载bar模块,按需加载
'echarts/src/chart/bar',
//'echarts/src/chart/pie'
],
function ($,ec,ecConfig) {});

但是这样会导致无法使用cmd模块,无法使用多类型模块循环引用。所以有需求时候使用System.amdRequire来音容AMD是最完美的,require用来加载CMD好了。

项目基本和手脚架设计

基本的情况文首说过一次,这里重复一下:
传统的多页面应用,不需要兼容旧浏览器,总体页面逻辑是顶部和右侧菜单不变,点击栏目链接不刷新更新中间区域,具体的业务逻辑这里不提,不是重点

这里提取和补充一下要点:

  • 左侧菜单和顶部菜单做成模块公用(不使用gulp来inject是因为涉及帐号权限问题),点击这菜单这两块不刷新
  • 中间区域通过jQuery的load使用ajax来载入进去,插入到内容区域,尽量减少全刷新。
  • CSS和JS在线和开发阶段使用不同版本,开发阶段方便调试不压缩合并,在线版本需要满足以下:
    • 压缩
    • 合并
    • 强缓存(即为资源添加版本号或者hash值,我个人喜欢hash)

文件目录基础设计

这里简单说下文件目录基础设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
├── asset
│   ├── css
│   ├── fonts
│   ├── images
│   └── js
├── config.js
├── html
│   ├── dist
│   │   └── login
│   │   ├── login.html
│   │   ├── logout.html
│   │   └── resetPwd.html
│   └── src
│   └── login
│   ├── login.html
│   ├── logout.html
│   └── resetPwd.html
└── modules
├── business
│   └── login
│   ├── login.js
│   ├── logout.js
│   └── resetPwd.js
├── github
├── npm
└── privateregistr

根据约定重于配置原则,这里说下设计和约定:

  1. 开发阶段使用html下src目录放置html文件,modules下business放置业务js和css代码,privateregistr这个随意命名的,用来放置公用模块,使用submodule维护。
  2. 发布阶段使用gulp对business下js和css进行打包,统一发布到asset/js和asset/css
  3. 每个html/dist下的文件夹为一个大栏目,里面每个html对应一个业务页面,每个页面的js名称结构要同modules/business下结构对应,举例来说:
    • html/dist/login/login.html这个文件的对应的js是modules/business/login/login.js
    • 每个页面只接受一个js作为业务入口,意思是如果上文login.js分解为step1.js&step2.js&step3.js是可以的,但是最终将只引入login.js一个,其他将require方式引用进去,不提供单独的script标签给它们。
  4. modules文件夹是jspm packages folder,里面的github和npm分别是github和npm下载的包,privateregistr则是使用jspm-git构建的私有包目标源。config.js是system.js的配置文件。

workflow

上面写了那么多,到底其实到底只是为了workflow服务。

这里简单说下这个流程下开发过程的展开:

开发

  1. 安装一个包

    1
    2
    # 安装一个做好模块化处理的jqueyr版本
    jspm install jquery=github:components/jquery
  2. 进入开发环节,假设要做个搜索页面,放在search大类目录下:

  • 新建文件:html/dist/search/search.html
  • 新建文件:modules/business/search/search.js
  • 开始编码环节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//js
...
module.exports = function(){
var $ = require("jquery")
$.get("XXXXX",{
query:"XXXX"
},function(data){
//coding here
})
}
...
//html
...
<!--build asset/css/search.css-->
<link rel="stylesheet" href="style1.css">
<link rel="stylesheet" href="style2.css">
<!--build-->

<script src="system.js"></script>
<script src="config.js"></script>
<!--inject:js-->
<!--endinject-->
<script>
System.import("modules/business/search/search",function(app){
app();
})
</script>
...

打包环节:

个人设计的打包环节主要是几个个主要部分组成:

  1. 处理 inject:js这个注释标签的,这里插入打包&压缩&强缓存处理后的js
  2. 处理 build这个注释标签,将内部css进行合并、压缩、强缓存的css并插入
  3. 第3个是第1个任务的依赖,它生成配置文件,用来设置好inject这个任务哪个html需要插入哪个js(这就是为什么要做html和js文件名和层级约定)
  4. 第4个也是第一个任务的依赖,它将所有js文件从business,进行打包压缩后放到asset/js目录下,并保持相应的目录层级

发布环节

将dist这类版本直接放到服务器即可。src这类目录为了安全还是不放上去的好

Gulp

这里为打包环节做个简单摘要,说下自己的task设计流程:

  1. 生成配置json文件——gulp json,主要用到了jsonfile和filewalker
  2. 进行模块打包,压缩——gulp jspm,主要用到了gulp-jspm
  3. 进行inject操作,处理css和js——gulp inject,主要用到了gulp-inject,gulp-hash,gulp-rev,gulp-rev-replace和gulp-usermin

总结

得益于前端变革般的发展,以及类似yeoman这类手脚架模板工具的普及和众多社区的贡献,优秀的手脚架貌似变得越来越多,有越来越廉价的感觉。但是这类手脚架一来往往都是基于SPA的设计,并不适合MPA来使用;二来是这类手脚架一般都是使用简单的requireJs这个加载器而非现在文章提及的可以支持UMD规范并有命令行工具支持的加载器,因此,并不能满足项目和业务快速迭代的需求,因此,有了这篇文章。

每一个被星星点亮手脚架其实都是无数经验积累的产物,愿大家可以在巨人的肩膀上更近一步。

本文只是一篇基于SystemJS工作流的总结。尚不敢妄称手脚架模板。

恩,文章对自己的这个流程已经有了非常丰富的记载,而gulp这个,限于篇幅,我决定单开一篇来介绍它。

————————-The End————————-