在SharePoint解决方案中使用JavaScript (0)
随着Web前段技术(JavaScript/HTML5)的日益发扬光大,在Web应用程序中,我们开始更多的使用JavaScript。很多以往是放在服务器上运行的逻辑,现在都开始逐渐的向前段转移。这种趋势不需要作者多说,只要是Web开发人员(包括SharePoint工程师),都会有所体验。而在SharePoint平台,这种前端化的趋势也是相当明显的。当我们构建SharePoint解决方案的时候,JavaScript代码的数量在不断的增多,而C#代码的数量则相对的不断减少,这个趋势毋庸置疑。
在我们的SharePoint应用中更多的使用JavaScript代码的优点有许多,其中我认为最重要的一条,也是我在设计SharePoint应用架构时最常考虑的一点,就是"放过w3wp"原则。w3wp.exe进程是SharePoint用来运行自身Web应用程序的主进程,而所谓"放过w3wp",就是说的我们所编写的自定义代码,应该尽量不要放到w3wp.exe进程(或者其它SharePoint用来运行自身代码的进程,比如用来运行Timer Job的进程)里面运行。许多SharePoint项目所面临的性能问题,都是因为开发团队所编写的C#代码质量不佳,或有内存泄漏的问题(比如没有正确的释放SPSite和SPWeb对象),或包含有不恰当的长时间运行的代码,这些代码一旦运行在w3wp.exe进程里面,轻则导致w3wp.exe进程占用的内存飞涨,重则让w3wp.exe进程hang掉。
更多的使用JavaScript,就是贯彻"放过w3wp"原则的重要手段之一。举一个很常见的场景,在SharePoint网站中的某个列表中,存储有需要让用户看到的通知信息,客户需求是希望用户打开网站时,在首页上就能看到他应该看到的通知。对于这个需求,一个理所当然的解决方案就是创建一个Web Part,这个Web Part使用C#代码,通过SPQuery等对象,从列表中查询到要显示给用户的通知,然后将通知显示在Web Part中。如果使用这个方案,我们的代码就是运行在服务器上的w3wp.exe进程里面,代码的一切潜在的问题,都需要w3wp.exe来承担。
利用SharePoint的沙盒解决方案(Sandboxed Solution),可以让我们的C#代码运行在服务器上单独的沙盒进程中,避免对w3wp.exe的影响,这是实现"放过w3wp"原则的一个方法。但是另外一个更灵活的方法,就是使用JavaScript前段技术,来实现我们所需要的功能。对于上面这个需求,开发人员完全可以仅仅依靠JavaScript和SharePoint的JavaScript Object Model,就从列表中查询到需要显示给当前用户的通知,然后将通知在首页上显示出来。由于这些JavaScript代码都是运行在用户的浏览器中,它们对于SharePoint服务器的冲击相比于C#代码,就小很多了。
所以,作为一名SharePoint工程师,了解如何在你的自定义SharePoint应用中用正确的方式使用JavaScript,是非常重要的。这个系列文章的目的也在于此,其中大部分内容都是作者本人在实践过程中,所总结的一些个人经验之谈。但是,这个系列的文章并不会讲述JavaScript这门语言本身以及SharePoint JavaScript Object Model,我们也不会讲述要如何正确的使用JavaScript语言 (虽然这对于工程师而言是非常非常重要的),已经有太多书籍和文章,讲述了JavaScript语言的各种最佳实践。
本文是系列文章的起始。
在SharePoint解决方案中使用JavaScript (1) – 引用.js文件
本文是系列文章的第一篇。
作为在SharePoint应用程序中使用JavaScript的第一步,就是要知道如何将一个写好的.js文件,引用到页面上。嗯,你可能觉得这个话题太简单了,"引用一个.js文件不就是在页面上方加一个<script>标签吗?"但是我们要考虑的事情,可通常要比这复杂得多。比如,我们大部分的.js文件,可能都是需要放置在网站中的所有页面上的,修改网站里面的每一个.aspx显然不是好主意,我们需要一个更好、更灵活的方案。
1、直接在母版页上引用.js文件
由于SharePoint网站中所有的页面,都默认使用同一个母版页(.master文件),所以在母版页上去引用一个.js文件,就会自动让所有使用这个母版页的网站页面,都引用到这个.js文件。在母版页的<head>区域,添加相应的<script>标签,来引用我们所需的.js文件,如下图所示。
(虽然上面的截图是使用了SharePoint Designer,但是我并不建议你在生产环境中,直接使用SPD来对母版页进行这样的修改。正确的做法应该是在Visual Studio项目中维护对自定义母版页的修改,并且以Module方式将自定义母版页发布到网站里面。)
在母版页中直接引用.js,简单、直接。这种方法非常适合引用一些几乎所有页面上都会用到的JavaScript库,比如jQuery。在母版页上完成了对jQuery的引用之后,网站中所有页面(和页面上的所有自定义Web Part)中的JavaScript代码,就都可以直接使用jQuery库了。整个项目中,如果有一些公用的自定义JavaScript库文件,也可以用这种方式来进行引用。
但是要注意的一点是,你需要认真考虑是否将.js文件的引用放到<head>区域中。这是因为页面在载入一个.js文件后,需要等待这个.js文件运行完成,才能继续后面HTML内容的渲染 (因为JS代码有可能修改DOM,所以得等到它运行完成,才能继续)。如果你的自定义.js文件中的代码非常复杂且耗时,那么会影响整个HTML页面的渲染速度 (即使服务器能很快的运行完成页面后端的代码并将HTML内容发送回客户端的浏览器)。所以如果不是一定需要,你可以将对.js文件的引用,放到页面的最后面,这样页面可以先将HTML内容渲染出来,然后再载入并运行.js文件。
<script>标签支持defer属性,这个属性用来告诉浏览器:你可以在完成HTML内容的加载之后再运行我。HTML5还给<script>标签又添加了一个async属性,来标记代码可以异步执行。但是由于浏览器兼容性的原因(你懂的),我建议你不要依赖这些属性。
2、使用SPWeb.CustomJavaScriptFileUrl属性
如果你希望在所有页面上都引用一个.js文件,而同时又不希望修改母版页,那么一个替代的方案是通过SPWeb.CustomJavaScriptFileUrl属性来做到。给这个属性设置一个.js文件的路径,母版页上的<SharePoint:CustomJSUrl>控件就会自动将那个.js文件载入到页面上。
下面的PowerShell脚本示范了如何为SPWeb.CustomJavaScriptFileUrl设置一个值。我使用PowerShell只是出于示范的目的,在你的项目中,这些代码通常会用C#实现,并很可能写在一个Feature的激活事件中,使得Feature被激活时,就设置好网站中所有页面需要载入的脚本。
SPWeb.CustomJavaScriptFileUrl属性的一个问题,就是它只能用来指定一个.js文件。但是这个问题很容易克服,我们可以让被引用的.js文件只是一个"启动器",它负责去载入其它必需的.js文件。就类似这样:
当然你也可以把这个"启动器"写得稍微复杂、灵活一点。
SPWeb.CustomJavaScriptFileUrl属性的另外一个问题,就是通过它所引用的.js文件,必然在<head>区域就被加载。之前说过,如果你的.js文件中的代码比较复杂耗时,那么它会影响到页面的整体加载速度。这个问题可以通过一些JavaScript的技巧来避免。如下图所示范的代码,代码首先立即载入jquery.js文件,然后在页面DOM加载完成之后,再载入其它的.js文件。
3、使用Custom Action
在大部分情况下,使用Custom Action的目的,都是为了向界面上添加自定义的菜单项或其它UI元素,但是Custom Action其实也是可以用来向页面上添加.js引用的。
在Visual Studio中,向SharePoint项目添加一个"空元素",然后如下所示,在<Elements>元素中添加一个<CustomAction>元素。"Location='ScriptLink'"属性告诉SharePoint,这个<CustomAction>的目的是为了向页面"注入"脚本。"ScriptSrc"属性用来指定要引用的.js文件的位置。在"ScriptSrc"属性里面,是可以使用"~site"和"~sitecollection"标记,用来表示网站和网站集的根目录的,比如:"ScriptSrc='~site/ScriptLibrary/Start.js'"。
如果要引用多个.js文件,也很好办,只需要添加多个<CustomAction>标签即可。注意每个<CustomAction>的"Sequence"属性,这个属性的值用来标识每个.js引用的载入顺序。SharePoint会按照"Sequence"属性所指定的顺序 (而并非<CustomAction>标签的声明顺序),依次引用.js文件。
Custom Action最大的好处,在于你可以将它放在一个Feature里面。用户激活Feature,网站就会引用Custom Action所指定的.js文件,用户停用Feature,这些.js文件就不会被引用。这是一个巨大的灵活性。
Custom Action的一个限制,就是它的"ScriptSrc"属性所引用的.js文件,要么位于_layouts中,要么位于网站或网站集里面。它是不能用来引用一个外部的.js文件的,比如,"ScriptSrc=' http://ajax.aspnetcdn.com/ajax/jquery/jquery-1.8.2.min.js'"是不会工作的。当然,你可以使用之前说过的"启动器"的方案,来解决这个问题,先通过Custom Action引用一个"启动器".js文件,然后在这个"启动器"里面去载入其它外部的.js文件。
4、自定义方案
如果上述的方案你都觉得不适合(或不够强大/灵活…),那么你可以构建一个自定义的.js文件载入方案。一个自定义的.js文件加载方案应该尝试解决如下问题:
- 可以引用网站内部、或外部任意位置的.js文件;
- 可以指定某个.js文件的加载时机:在<head>区域/DOM加载完成之后/整个页面加载完成之后等等
- 可以指定将某个.js文件只加载到特定页面,比如:列表的Forms页面/列表的新建列表项页面(New Form)/某个特定URL的页面
- 可以指定多个.js文件之间的依赖关系,并按照依赖关系顺序加载
文本概要的讲述了在SharePoint网站中引用.js文件的几种常见方式,在后面的文章里面,我们将继续讲述将.js文件放置到哪里比较合适、如何在Web Part上使用JavaScript脚本等话题。
在SharePoint解决方案中使用JavaScript (2) – 模块化
本文是在SharePoint中使用JavaScript的第二篇文章,前面的文章包括:
理论上,不管你是在哪个场景中编写JavaScript代码,都应该让你的代码模块化。JavaScript代码是非常容易变成一团乱麻的,特别是在你没有将代码进行模块化的情况下。在SharePoint中使用JavaScript也同样如此。一些基本的JavaScript模块化的原则包括:
- 尽量让每一个.js文件都是一个模块
- 每个模块都可以有只属于自己的"私有"数据和函数,模块只暴露必要的数据和方法出去
- 模块之间存在依赖关系
- 通过某个加载方案,使模块能按照正确的顺序(通常是它们的依赖关系)被加载
下面介绍两种常见的JavaScript模块化的方法,以及所对应的加载方案。
1、最简模块化
最简单的一种方式,就是直接使用JavaScript的匿名函数。通过将整个模块都放在一个立即执行的匿名函数里面,我们就可以获得一个独立的"执行空间"。在下面的示例中,我们可以在模块中定义"私有"的变量和函数,然后将需要暴露的内容注册到一个全局的"命名空间"MyNameSpace里面。由于JavaScript实际上并没有命名空间的概念,所以我们的命名空间,实际上也是通过定义一个全局变量实现的。将整个应用程序的所有模块都注册到同一个"命名空间"里面,可以尽量减少全局变量的使用(理论上来说,除了这个命名空间本身,就不需要注册其它全局变量了)。
这种命名空间的定义方式,有时候也会写成如下图那个样子。在下图的例子中,模块直接暴露了一个构造函数。
将命名空间传递给匿名函数的参数的好处是,在其它模块中,可以通过参数所传递进来的命名空间,很自然的调用另一个模块暴露出来的接口。
这种最简模块化定义方式最大的好处,就是它不需要依赖任何第三方的库,具有非常好的兼容性。你可以将一个模块文件从一个项目复制到另外一个项目,(除了要修改一下模块注册到的命名空间之外,)很可能就可以直接使用了。
管理模块间的依赖关系,并按照依赖关系载入这些模块,是开发人员需要考虑的问题。你既可以用一些简单的方法(比如直接将所有模块文件都引用到页面上,如果模块比较少的话),或者创建一些自定义的方案来进行管理。
下面是一个简单的例子,对一个应用程序中所有的模块进行声明,并定义它们之间的依赖关系,然后通过一个自定义的脚本载入器,根据声明按照顺序载入所有的模块。嗯,具体载入器的实际代码就不附上了,这个只是随手写的一个例子。实际上,只要有了模块的声明,使用LazyLoad之类的函数来进行模块加载,并不难实现。
2、AMD(Asynchronous Module Definition)模块化
除了使用不依赖任何第三方库的匿名函数对模块进行封装之外,当然还可以使用AMD规范来进行模块的定义。支持AMD规范的库很多,比如著名的RequireJS。下面就是一个基于RequireJS所定义的模块。
当然,一旦决定了使用哪个AMD库,那么所有模块就需要使用那个库所要求的样子,所幸大部分基于AMD规范的库对于模块的定义要求都是非常类似的。RequireJS能够根据模块所声明的依赖,在加载一个模块时,自动加载它所依赖的其他模块。关于RequireJS的更详细信息,请参考它的官方网站。
本文只介绍了两种进行JavaScript模块化的方案。当然,进行模块化还有很多其他的方案,市面上存在着许多类似的库。很多更复杂的JavaScript框架里面(比如AngularJS),甚至会直接包含有模块化的功能。微软的TypeScript直接内置了module这个关键字(TypeScript好像是尽量模拟ES Harmony,下一个JavaScript语言规范版本中的用法),来支持模块化。基于何种方案进行JavaScript模块化,需要你根据各个因素,选择一种对你的应用程序来说最佳的方案。根据我的经验,基本上所有方案都应该能很好的和SharePoint一块儿工作。