去年一年里,我与一些大中型公司进行了几次讨论。这些公司所感兴趣的是建立一些公共用户界面库,这样公司所有的开发团队都能共用。很多时候,每个团队都是在不同的框架栈上搞开发,如React,Vue,Angular等等。这些框架都能理解消化网站组件(Web Components),并与组件进行互通,所以我能理解他们为什么都在使用网站组件技术和Polymer库之类的工具。
多数JavaScript框架往往要依靠ES模块标准格式,或CommonJavaScript标准格式来写模块代码,同时还要依靠Webpack模块打包器等工具来打包代码。 而Polymer库使用的则是HTML导入技术(HTML Imports),这就意味着,想两种工具同时使用,所有部分打包的方式、加载的顺序开发人员必须都协调好。
不过Webpack打包器本身已经能够理解多种文件格式了,如.css,.tsx文件等。那么能不能将这个功能扩展一下,让它也能理解导入的HTML文件呢?当然行,为什么不行?今天我怀着十分激动的心情,来给大家炫耀一下一个新工具polymer-webpack-loader加载器,它能理解消化HTML文件的导入,并输出Webpack能打包的模块。Polymer库有了这个工具,在别的框架中不仅使用起来更方便,而且还多了一些有趣的新特色。比如,有些模块是通过npm安装的,现在Polymer库就能用ES模块格式的导入(import
)句法来把这些模块的代码拉进来。
趁现在内容还没有跳掉太多,我想指出一点,这个工具并不是官方Polymer项目的一部分。 Polymer并不会“ 全面应用于Webpack加载器上”,也没有任何类似的功能。它只是由两个社区成员Bryan Coulter和Chad Killingsworth创作的一个工具而已,虽然非常棒。它可以解决一些开发人员烦恼的问题,虽然还处于测试阶段,我觉得稍微写一点东西还是不错的,这样Polymer社区的成员能在正式推出前先大致看看有没有什么疏漏,给一些反馈意见。
这个工具是给谁用的?
谈实质问题前,我想先说说从中受益最多的该是什么样的用户。如果已经在项目使用Polymer CLI命令行工具或polymer-build生成库,并且觉得挺顺利的,那当然就不要动了。这些工具都是非常棒,效果非常好的。😁
polymer-webpack-loader模块加载器对以下用户最有用:
-
想将Polymer库中的元素整合进一个更大的项目里去,而这个项目又使用了一个不同的JavaScript框架。
- 想在Polymer元素里使用
import
导入句法,对npm模块包进行补充。 -
想使用TypeScript语言,JSX语言,emojiscript颜文字语言和[其它你喜欢的自选语言]。
工作原理如何?
polymer-webpack-loader加载器使用HTML导入技术,获取导入的文件,再把整个东西转换成一个JavaScript模块。整个过程分三个步骤:
第一步
All 把所有的<link>
链接元素都转换成ES模块格式的import
导入语句。比如说,<link rel="import" href="paper-button.html">
,这个元素就会变成import ‘paper-button.html’;
语句。看起来可能有些怪,但是要记住,在Webpack中,所有的东西都是模块,甚至连HTML和CSS文件也是模块。将<link>
元素转换成import
导入语句后,Webpack库就能顺着它们检索,弄清其它模块依存关系了。
第二步,将<dom-module>
元素转成字符串模板
Polymer的<dom-module>
元素是一个自定义元素(Custom Element),用以获取包含的元素模板,再将模板置于一个全局映射表中。每创造一个Polymer元素,第一件事就是在表中找到模板并勾去。polymer-webpack-loader加载器只是将<dom-module>
元素中的内容取出,再转换成一个字符串形式的模板,之后执行打包时,再把这个字符串模板导入。实质上讲,最终结果是一样的,都是创建一个全局映射表,使元素的模板能被访问,只是方法稍有不同。
第三步,把<script>
脚本元素分离出来
最后一步,所有的<script>
脚本标签,如果包含有效的src
属性,就会被转成'import'
导入语句。这样做,那些嵌入式脚本就被分离了出来,加载链中的其它的加载器像babel-loader或ts-loader就能处理了。 最后,所有部分包括这些脚本都一起扔给bundle.js
文件。
最终结果是一个bundle.js
文件,里面汇集了有所有的元素和所有其它依存部分。
让我们做一个hello world例子吧
一开始先用npm安装这个加载器。
npm install --save-dev polymer-webpack-loader
如果用的是上面这个演示项目,那也可以cd
命令进入到项目文件夹,再运行npm i
命令和bower i
命令。
下一步,将加载器加入到webpack.config.js
文件里去,以下是整个配置文件,直接从演示项目复制过来的。我尽力给每个部分都加上注释,这样即使是新用户也能了解一下怎么回事。
/* webpack.config.js配置文件 */
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var path = require('path');
module.exports = {
// 告诉Webpack哪个文件可以启动程序。
entry: path.resolve(__dirname, 'src/index.js'),
// 告诉Weback打包到./dist/bundle.js这个里去文件
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
// 告诉Webpack:在解析导入语句时,要查看哪些文件夹。
// 一般Webpack在默认状态下,会查找node_modules文件夹,但这里还有一个文件夹bower_components要查找,它把默认的给覆盖了,所以还要再加上默认的node_modules文件夹。
resolve: {
modules: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'bower_components')
]
},
// 下面这几条规则告诉Webpack如何处理不同的模块类型。
// 记住,在Webpack里*所有的*东西都是模块。包括
// CSS文件,还有加载器带来的HTML文件。
module: {
rules: [
{
// 看见以.html结尾的文件,发给下面这些加载器。
test: /\.html$/,
// 这是Webpack加载链的例子。
// 加载链中的加载器,从最后一个开始依次运行,直到第一个。所以先运行的是
// polymer-webpack-loader加载器,然后把生成的结果传给
// babel-loader加载器。靠这个加载器,我们能把`<script>`脚本元素中的JavaScript代码转换编译过去。
use: [
{ loader: 'babel-loader' },
{ loader: 'polymer-webpack-loader' }
]
},
{
// 看到.js结尾的文件,只要直接发送给babel-loader加载器就行了。
test: /\.js$/,
use: 'babel-loader'
}
]
},
// 启动Webpack开发服务器,项目发生变化时,服务器能生成、支持和重载项目了。
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000
},
plugins: [
// 这个插件能为我们生成一个索引页index.html文件,可以由
// Webpack开发服务器所使用。我们用EJS语言写一个模板文件,发给这个插件。
// 然后这个插件就能将打包过的模块插进去。
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.ejs')
}),
// 这个插件会把所有的文件都复制到‘./dist’文件夹去,不进行转换。
// 这一点很重要,因为custom-elements-es5-adapter.js文件必须
// 保持ES2015版格式不变。过一会我们会讲到这一点:)
new CopyWebpackPlugin([{
from: path.resolve(__dirname, 'bower_components/webcomponentsjs/*.js'),
to: 'bower_components/webcomponentsjs/[name].[ext]'
}])
]
};
要注意的关键点是module
模块部分,这个部分定义了一系列规则。其中第一条检测文件是否以.html
结尾,如果是,文件就发送到加载链去。链中的加载器都会以某种方式转换文件,这有点像其它生成工具中的“任务”。这里我们的任务就是“全都通过polymer-webpack-loader加载器来处理,得到的输出结果再给babel-loader加载器处理。”
接下来,程序需要一个启动点,那么就创建一个index.js
文件,加上一个import
导入语句,把要用HTML导入的文件拉进来。
`/* src/index.js */`
`import './my-element.html';`
这里是my-element.html
文件的实际定义。
<!-- src/my-element.html -->
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<dom-module id="my-element">
<template>
<h1>Hello, World! It's [[today]].</h1>
</template>
`<script type=”module”>`
// 耶,我们把一个Node模块拉进来了
import format from 'date-fns/format';
class MyElement extends Polymer.Element {
static get is() { return 'my-element'; }
static get properties() {
return {
today: {
type: String,
value: function() {
return format(new Date(), 'MM/DD/YYYY');
}
}
}
}
}
window.customElements.define(MyElement.is, MyElement);
</script>
</dom-module>
my-element.html
本身是一个非常普通的Polymer元素,不过有一个功能很有意思的。在<script>
元素里,导入了date-fns日期功能库。这个库在node_modules
文件夹里的是哪个版本,Webpack就会以此版本来解析,并将它编译好打包,非常贴心。最终,我们不仅能在元素定义里补充模块,而且保证这个模块的作用范围的适当的!
最后一部分,我们要组装起一个index.ejs
模板,由Webpack开发服务器所实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<my-element></my-element>
<!--
功能检测,看一下是否支持自定义元素。如果浏览器确实支持自定义元素,那就需要载入custom-elements-es5-adapter适配器,因为之前项目中的代码已由ES2015版转编译成ES5版格式,并且本地的自定义元素会注册成类。
-->
<div id="ce-es5-shim">
`<script type="text/javascript">`
if (!window.customElements) {
var ceShimContainer = document.querySelector('#ce-es5-shim');
ceShimContainer.parentElement.removeChild(ceShimContainer);
}
</script>
`<script type="text/javascript" src="bower_components/webcomponentsjs/custom-elements-es5-adapter.js">`</script>
</div>
<!--
用webcomponents-loader加载一个脚本。这个脚本做功能检测,看哪些网站组件相关功能缺失。如果有不支持的API,就延迟加载合适的库,填补空缺,打包起来。
补充库打包完毕,侦听到'WebComponentsReady'事件时,就可以插入'bundle.js'文件.
-->
<script>
(function() {
document.addEventListener('WebComponentsReady', function componentsReady() {
document.removeEventListener('WebComponentsReady', componentsReady, false);
var script = document.createElement('script');
script.src = '<%= htmlWebpackPlugin.files.js[0] %>';
var refScript = document.getElementsByTagName('script')[0];
refScript.parentNode.insertBefore(script, refScript);
}, false);
})();
</script>
`<script src="bower_components/webcomponentsjs/webcomponents-loader.js">`</script>
<!--
重点:在HTMLWebpackPlugin插件里,一定要有inject: false这一选项,否则这个插件会把bundle.js文件也插进去。对于bundle.js文件的加载,我们已经在之前自己处理了。
-->
</body>
</html>
这个index.ejs
模板要加载自定义元素的ES5格式适配器、网站组件功能补充库和Webpack打包生成的bundle.js
文件。另外还在<body>
部分包含了一个<my-element>
实例。
不清楚什么是自定义元素ES5格式适配器?看这个视频,有全面解释。
注意有一行写着<%= htmlWebpackPlugin.files.js[0] %>
,是我们的bundle.js
最终所处的位置。
最后,在终端运行npm start
命令来启动Webpack开发服务器,它会自动打开浏览器窗口。应该看到这样的画面:
成功!现在Polymer与Webpack打包在了一起,并且补充了从node_modules
文件夹中导入的模块。
开放问题
我用这个加载器写的元素,能不能发布到WebComponents.org上去?
看情况。如果是利用ES模块格式的import
导入句法把npm模块包拉进来的话,那么就不行,否则,不论谁想用你的元素都必须同时使用Webpack库。我觉得这个工具最好是用于处理你自己的应用程序,以及一些并不想分享的自定义元素。
是不是说我就不用再使用bower管理了?
不一定。Polymer库以及一般网站组件为什么要靠bower管理,这是因为自定义元素的标签是全局的。这意味着一个元素注册时,不能同时有多个冲突的版本。而Bower管理安装时,会强制消除重复版本冲突,所以每个依存部分最后总是只有一个版本。npm不支持这个功能,所以目前来讲,可能最好还是继续使用bower来安装网站组件。如果对此有兴趣,想知道得更多的话,看看我最近在I/O大会上的讲话视频。
PRPL模式(推送,渲染,预缓存,延迟加载),还有代码分割是怎么回事?
HTML导入技术有许多特点,我最喜欢的一点是它鼓励使用者将所有与一个网站组件有关的HTML,CSS和JavaScript都归在一个文件里。这样,很容易就能区分哪个组件是浏览器初次渲染所必须的,哪些可以慢些渲染。把组件按必须与否打包成一个个独立的“碎片”,可以用Polymer CLI之类的工具,然后再用Polymer的importHref
方法来按需要延迟加载。
Webpack同样可以做到这一点,有一个叫代码分割(Code Splitting)的功能。 polymer-webpack-loader加载器的创作人员编纂了一个不错的Polymer入门套件例子,演示了如何配置Webpack来进行代码分割,还是如何将Polymer的importHref
方法换成Webpack的
动态导入函数[import()](https://github.com/Banno/polymer-2-starter-kit-webpack/blob/master/src/my-app.html#L127-L138)
。
结论
我觉得Polymer生成的自定义元素绝对可以在任何库或框架下运行。但是这个过程经常不是很顺利,有时问题出在生成工具上,这使得整个过程感觉过于麻烦,不值得去做。我看到polymer-webpack-loader加载器时是很兴奋的,它解决了我经历过的一个大麻烦。我也等不及想看到越来越多的人用它来把自定义元素引入到项目中去,并在团队间分享。
非常感谢Bryan Coulter和Chad Killingsworth创作了polymer-webpack-loader加载器。也感谢对Sean Larkin本帖的审查。