扩展新端编译手册
扩展新端编译插件的使用手册。
1 脚手架
安装chameleon-tool@1.0.3
进行扩展新端的开发。
2 扩展新端总体编译流程
扩展新端 首先要了解扩展新端总体的编译流程,理解用户扩展新端的工作处于编译的什么阶段。
- 黄色部分表示cml脚手架及webpack编译
- 蓝色部分表示扩展的mvvm+编译
- 绿色部分表示用户要实现的部分,整个项目的文件会用一张编译图表示,用户提供针对每一个节点的编译处理,全部编译完成后,进行文件的拼接打包。
3 编译图与编译节点
在总体编译流程中,webpack编译完成后,会将webpack编译的结果转成标准的mvvm编译图projectGraph,这个图由CMLNode节点构成,先理解编译图的组织形式和节点的数据结构,对用户写编译插件有很大帮助。
3.1 编译节点
编译节点是CMLNode类的实例,CMLNode定义如下:
class CMLNode {
constructor(options = {}) {
this.ext;
this.realPath; // 文件物理地址 会带参数
this.nodeType; // app/page/component/module // 节点类型 app/page/component 其他的为module cml文件中的每一个部分也是一个Node节点
this.moduleType; // template/style/script/json/asset
this.dependencies = []; // 该节点的直接依赖 app.cml依赖pages.cml pages.cml依赖components.cml js依赖js
this.childrens = []; // 子模块 cml文件才有子模块
this.parent; // 父模块 cml文件中的子模块才有
this.source; // 模块源代码
this.convert; // 源代码的格式化形式
this.output; // 模块输出 各种过程操作该字段
this.identifier; // 节点唯一标识
this.modId; // 模块化的id requirejs
this.extra; // 节点的额外信息
Object.keys(options).forEach(key => {
this[key] = options[key];
})
}
}
具体字段含义如下:
字段 | 含义 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
nodeType | 节点类型,分为app/page/component/module,其中只有src/app/app.cml类型为app, router.config.json中配置的cml文件为page,其他的cml文件为component。非cml文件为Module | ||||||||||||
moduleType |
模块类型,当节点的nodeType为app/page/component时,其moduleType为undefined。cml文件中四个部分的moduleType分别为template、script、style、json。其他节点的nodeType为module时,根据文件后缀判断moduleType。
|
||||||||||||
dependencies | 节点的依赖节点,app依赖page page依赖component script节点中依赖require的节点 | ||||||||||||
childrens | 节点的子节点,只有cml文件才会有子节点,子节点为cml文件的四个部分,分别为四个节点 | ||||||||||||
parent | 节点的父节点,只有cml文件节点的子节点才有父节点 | ||||||||||||
originSource | 节点编译前源代码(目前只有script节点有该字段) | ||||||||||||
source | 经过mvvm标准编译之后节点的代码 | ||||||||||||
convert | source的转换格式,source均为字符串,convert可能装成AST或者JSON对象 | ||||||||||||
output | 节点的输出内容,建议用户编译可以将编译结果放在output字段用于输出 | ||||||||||||
identifier | 节点的唯一标识,是webpack module中的request字段,保证了唯一性 | ||||||||||||
modId | 节点的模块id,用于js的模块化id标识 | ||||||||||||
extra | 节点的额外信息,例如template节点就会添加上模板使用的原生组件和内置组件信息 | ||||||||||||
ext | 文件后缀,例如.js 注意如果引用资源后有参数也会带着,例如 .png?__inline | ||||||||||||
realPath | 节点对应的文件路径,注意如果引用资源后有参数也会带着,例如 /user/didi/yyl/project/chameleon.png?__inline |
3.2 编译图组织结构
编译图由上节介绍的编译节点组成,以nodeType为app的节点开始形成编译图,内部会递归编译节点,根据节点的类型触发相应的用户编译。
4 如何编写用户插件
扩展内置组件库和内置API库是独立的两个NPM包,其他编译相关的工作都放在用户插件中。
4.1 确定端标识名称
扩展一个新端首先要确定这个端的标识名称,例如微信小程序端为wx
,百度小程序端为baidu
,这个标识决定了构建命令的名称、多态协议中的cmlType, 配置对象中的cmlType等。
4.2 配置插件
在项目的chameleon.config.js中配置构建目标端命令时要执行的插件,这里配置的只是插件的名称,后面会讲解插件的写法。配置的字段为extPlatform
Object类型,key值为上一步确定的端标识名称,value为要实现的插件的npm包名称, 例如要扩展头条小程序,确定标识为toutiao
。
cml.config.merge({
extPlatform: {
toutiao: 'cml-toutiao-plugin'
}
})
当执行cml 端标识名称 dev|build
时将走用户插件进行编译。
4.3 扩展新端插件
上一步讲解了如何配置端标识命令对应的用户插件,这里讲一下插件该如何编写。插件是一个类
,要求是npm包的入口。下面展示出这个类的属性和方法。
module.exports = class ToutiaoPlugin {
constructor(options) {
let { cmlType, media} = options;
this.webpackRules = []; // webpack的rules设置 用于当前端特殊文件处理
this.moduleRules = []; // 文件后缀对应的节点moduleType
this.logLevel = 3;
this.originComponentExtList = ['.wxml']; // 用于扩展原生组件的文件后缀查找
this.runtimeNpmName = 'cml-demo-runtime'; // 指定当前端的运行时库
this.builtinUINpmName = 'cml-demo-ui-builtin'; // 指定当前端的内置组件库
this.cmlType = cmlType;
this.media = media;
this.miniappExt = { // 小程序原生组件处理
rule: /\.wxml$/,
mapping: {
'template': '.wxml',
'style': '.wxss',
'script': '.js',
'json': '.json'
}
}
// 需要压缩文件的后缀
this.minimizeExt = {
js: ['.js'],
css: ['.css','.wxss']
}
}
/**
* @description 注册插件
* @param {compiler} 编译对象
* */
register(compiler) {
/**
* cml节点编译前
* currentNode 当前节点
* nodeType 节点的nodeType
*/
compiler.hook('compile-preCML', function(currentNode, nodeType) {
})
/**
* cml节点编译后
* currentNode 当前节点
* nodeType 节点的nodeType
*/
compiler.hook('compile-postCML', function(currentNode, nodeType) {
})
/**
* 编译script节点,比如做模块化
* currentNode 当前节点
* parentNodeType 父节点的nodeType
*/
compiler.hook('compile-script', function(currentNode, parentNodeType) {
})
/**
* 编译template节点 语法转义
* currentNode 当前节点
* parentNodeType 父节点的nodeType
*/
compiler.hook('compile-template', function(currentNode, parentNodeType) {
})
/**
* 编译style节点 比如尺寸单位转义
* currentNode 当前节点
* parentNodeType 父节点的nodeType
*/
compiler.hook('compile-style', function(currentNode, parentNodeType) {
})
/**
* 编译json节点
* currentNode 当前节点
* parentNodeType 父节点的nodeType
*/
compiler.hook('compile-json', function(currentNode, parentNodeType) {
})
/**
* 编译other类型节点
* currentNode 当前节点
*/
compiler.hook('compile-other', function(currentNode) {
})
/**
* 编译结束进入打包阶段
*/
compiler.hook('pack', function(projectGraph) {
// 遍历编译图的节点,进行各项目的拼接
//调用writeFile方法写入文件
// compiler.writeFile()
})
}
}
下面对插件类中的每一个属性和方法的使用进行介绍。
插件构造函数参数
constructor(options) {
let { cmlType, media} = options;
}
用户插件的构造函数会接受options
参数,cmlType
是当前端标识名称,例如web|wx|weex
, media
是构建的模式,dev|build
。
this.logLevel
类型:Number 日志的等级, 可取值0,1,2,3,默认值为2,值越大显示日志越详细。
this.originComponentExtList
类型:Array 用于设置原生组件的文件后缀,适用于多态组件的查找。 例如 usingComponents中对于组件的引用是没有后缀的,用户可以对其进行扩展,再进行组件查找时会尝试用户设置的后缀,一般用于多态组件调用底层原生组件。 例如微信小程序中:
this.originComponentExtList = ['.wxml']; // 用于扩展原生组件的文件后缀查找
this.runtimeNpmName
类型:String 用于设置当前端运行时npm包名称。
this.builtinUINpmName
类型:String 用于设置当前端内置组件npm包名称。
this.minimizeExt
类型:Object
{
js: Array,
css: Array
}
内置了两种代码压缩,一种是js 一直是css,用户指定输出文件后缀对应的压缩类型。例如微信小程序中:
this.minimizeExt = {
js: ['.js'],
css: ['.css','.wxss']
}
this.miniappExt
类型:Object chameleon内置了针对小程序类的原生组件的处理方法,只需要用户进行文件后缀的配置。例如微信小程序:
this.miniappExt = { // 小程序原生组件处理
rule: /\.wxml$/,
mapping: {
'template': '.wxml',
'style': '.wxss',
'script': '.js',
'json': '.json'
}
}
rule的正则匹配文件后缀和this.originComponentExtList
中的设置的文件后缀保持一致。
mapping中的四个部分配置小程序对应的文件后缀。
this.webpackRules
类型:Array 当用户有其他文件类型的原生组件要处理,可以通过配置weback的module.rules字段,用于扩展目标端特殊文件类型的处理,用户可以扩展webpack编译过程的loader。例如:
this.webpackRules = [{
test: /\.vue$/,
use: [{
loader:'vue-loader',
options: {}
}]
}]
在loader中可以设置this._module中的一些字段控制生成的CMLNode
的内容。
- this._module._cmlSource 设置
CMLNode
的source字段 - this._module._nodeType 设置
CMLNode
的nodeType字段 - this._module._moduleType 设置
CMLNode
的moduleType字段 - this._module._cmlExtra 设置
CMLNode
的extra字段
this.moduleRules
类型:Array 设置文件类型对应的moduleType,可以配合webpackRules使用,内置对应关系如下:
[ // 文件后缀对应module信息
{
test: /\.css|\.less|\.stylus|\.styls$/,
moduleType: 'style'
},
{
test: /\.js|\.interface$/,
moduleType: 'script'
},
{
test: /\.json$/,
moduleType: 'json'
},
{
test: /\.(png|jpe?g|gif|svg|mp4|webm|ogg|mp3|wav|flac|aac|woff|woff2?|eot|ttf|otf)(\?.*)?$/,
moduleType: 'asset'
}
]
例如用户可以扩展.vue
类型的moduleType为vue
。
this.webpackRules = [{
test: /\.vue$/,
moduleType: 'vue'
}]
在递归触发用户编译阶段的钩子名称,也是根据节点的moduleType
决定,所以用户扩展了节点的moduleType
,相应这个节点触发的编译钩子也为compile-${moduleType}
, 上面的例子中触发compile-vue
。
register方法
register方法中接受compiler
对象,该对象是编译的核心对象,用户通过该对象注册编译流程。
compiler.hook方法
使用该方法可以注册编译流程,第一个参数是钩子名称,第二个参数是处理函数,处理函数中会接收编译流程对应的参数,下面说明每一个钩子的作用和参数。
compiler.hook(钩子名称, function(参数) {
})
1 compile-preCML
参数列表:(currentNode,nodeType)
- currentNode 当前处理的节点
- nodeType 当前节点的nodeType,app/page/component
说明:
这个钩子是编译cml文件节点之前触发,并且传递cml文件节点,可以通过该钩子去处理cml文件节点的template、json、style、script
四个子节点之前需要的联系。
2 compile-postCML
参数列表:(currentNode,nodeType)
- currentNode 当前处理的节点
- nodeType 当前节点的nodeType,app/page/component
说明:
这个钩子是编译完cml文件节点的依赖和子节点后触发,传递cml文件节点,可以通过该钩子去处理cml文件节点编译之后的处理。
3 compile-script
参数列表:(currentNode,parentNodeType)
- currentNode 当前处理的节点
- parentNodeType 父节点的nodeType,如果是app/page/component节点的子节点会有值,否则为undefined
说明:
这个钩子用于处理nodeType='module'
,moduleType='script'
的节点,内部已经对js文件进行了babel处理,这个阶段用于做模块的包装,compiler.amd
对象提供了amd模块的包装方法,模块id使用节点的modId
字段。例如:
compiler.hook('compile-script', function(currentNode, parentNodeType) {
currentNode.output = compiler.amd.amdWrapModule(currentNode.source, currentNode.modId);
})
4 compile-template
参数列表:(currentNode,parentNodeType)
- currentNode 当前处理的节点
- parentNodeType 父节点的nodeType,如果是app/page/component节点的子节点会有值,否则为undefined
说明:
这个钩子用于处理nodeType='module'
,moduleType='template'
的节点,如果模板是类vue语法,内部已经将其转为标准的cml语法,这个阶段用于对模板语法进行编译,生成目标代码,转义可以采用mvvm-template-parser
npm包提供的方法,可以将模板字符串转为ast语法树进行操作。例如:
const {cmlparse,generator,types,traverse} = require('mvvm-template-parser');
compiler.hook('compile-template', function(currentNode, parentNodeType) {
let ast = cmlparse(currentNode.source);
traverse(ast, {
enter(path) {
//进行转义
}
});
currentNode.output = generate(ast).code;
})
5 compile-style
参数列表:(currentNode,parentNodeType)
- currentNode 当前处理的节点
- parentNodeType 父节点的nodeType,如果是app/page/component节点的子节点会有值,否则为undefined
说明:
这个钩子用于处理nodeType='module'
,moduleType='style'
的节点,内部已经对less stylus等语法进行编译处理,这里得到的已经是标准的css格式,可以转成对应端的样式,比如对尺寸单位cpx的转换,将css转成对象形式等。例如:
compiler.hook('compile-style', function(currentNode, parentNodeType) {
//利用编写postcss插件的形式进行转义
let output = postcss([cpx()]).process(currentNode.source).css;
currentNode.output = output;
})
6 compile-json
参数列表:(currentNode,parentNodeType)
- currentNode 当前处理的节点
- parentNodeType 父节点的nodeType,如果是app/page/component节点的子节点会有值,否则为undefined
说明:
这个钩子用于处理nodeType='module'
,moduleType='json'
的节点。例如:
compiler.hook('compile-json', function(currentNode, parentNodeType) {
let jsonObj = currentNode.convert;
jsonObj.name = "用户自定义操作"
currentNode.output = JSON.stringify(jsonObj);
})
6 compile-other
参数列表:(currentNode)
- currentNode 当前处理的节点
说明:
这个钩子用于处理nodeType='module'
,moduleType='other'
的节点。对于不是cml识别的模块类型进行编译。
7 compile-asset
参数列表:(currentNode,parentNodeType)
- currentNode 当前处理的节点
- parentNodeType 父节点的nodeType,如果是app/page/component节点的子节点会有值,否则为undefined
说明:
这个钩子用于处理nodeType='module'
,moduleType='asset'
的节点,资源节点内部已经将其source转为js语法,返回资源的publichPath
,所以将其等同于script
节点进行处理。例如:
compiler.hook('compile-script', function(currentNode, parentNodeType) {
currentNode.output = compiler.amd.amdWrapModule(currentNode.source, currentNode.modId);
})
8 pack
参数列表:(projectGraph)
- projectGraph 编译图根节点
说明:
所有编译结束之后触发这个钩子,在这个钩子中编译图,拼接目标端的文件内容,调用compiler.writeFile()
方法写入要生成的文件路径及内容。
compiler.hook('pack', function(projectGraph) {
// 遍历编译图的节点,进行各项目的拼接
//调用writeFile方法写入生成文件
compiler.writeFile('/app.json',projectGraph.output)
})
compiler.amd
compiler.amd对象提供了js语言的AMD模块化方案供开发者使用,
ompiler.amd.amdWrapModule
参数列表 ({content, modId})
- content js模块的内容
- modId 该模块的Id
说明: 将js模块包装成amd模块。
例如modId为src/pages/index/index.cml
,content为如下的模块:
class Index {
data = {
title: "chameleon",
chameleonSrc: require('../../assets/images/chameleon.png')
}
}
export default new Index();
调用compiler.amd.amdWrapModule后返回的结果为:
cmldefine('src/pages/index/index.cml', function(require, exports, module) {
class Index {
data = {
title: "chameleon",
chameleonSrc: require('../../assets/images/chameleon.png')
}
}
export default new Index();
})
compiler.amd.getGlobalBootstrap
参数列表 (globalName)
- globalName 环境的全局变量
说明: 该方法返回js amd模块方案的启动脚本,这个脚本是将cmldefine和cmlrequire都放到用户传递的全局变量上。返回代码如下:
(function(cmlglobal) {
cmlglobal = cmlglobal || {};
cmlglobal.cmlrequire;
var factoryMap = {};
var modulesMap = {};
cmlglobal.cmldefine = function(id, factory) {
factoryMap[id] = factory;
};
cmlglobal.cmlrequire = function(id) {
var mod = modulesMap[id];
if (mod) {
return mod.exports;
}
var factory = factoryMap[id];
if (!factory) {
throw new Error('[ModJS] Cannot find module `' + id + '`');
}
mod = modulesMap[id] = {
exports: {}
};
var ret = (typeof factory == 'function')
? factory.apply(mod, [require, mod.exports, mod])
: factory;
if (ret) {
mod.exports = ret;
}
return mod.exports;
};
})($GLOBAL); // 全局变量
compiler.amd.getModuleBootstrap
无参数。
说明: 某些平台没有提供全局变量,所有amd的启动脚本也提供了模块化的方案。返回如下代码:
/**
* 模块型
*/
(function() {
var factoryMap = {};
var modulesMap = {};
var cmldefine = function(id, factory) {
factoryMap[id] = factory;
};
var cmlrequire = function(id) {
var mod = modulesMap[id];
if (mod) {
return mod.exports;
}
var factory = factoryMap[id];
if (!factory) {
throw new Error('[ModJS] Cannot find module `' + id + '`');
}
mod = modulesMap[id] = {
exports: {}
};
var ret = (typeof factory == 'function')
? factory.apply(mod, [require, mod.exports, mod])
: factory;
if (ret) {
mod.exports = ret;
}
return mod.exports;
};
module.exports = {
cmldefine,
cmlrequire
}
})();
compiler.writeFile
参数列表 (filePath, content)
- filePath 文件路径相对路径,会在项目根目录
dist/${端标识}
下拼接上filePath。 - content 输出的文件内容
String or Buffer
说明: 在pack
钩子中通知用户所有节点已经编译完成,用户可以遍历projectGraph
编译图,进行目标文件的拼接,调用compiler.writeFile
方法进行写出,pack
钩子执行完毕后,内部编译将输出文件。例如:
let appJson = {
window: {
"navigationBarTitleText": "Chameleon"
}
}
compiler.writeFile('/app.json', JSON.stringify(appJson))
compiler.getRouterConfig
参数列表: 无参数 返回值结构:
{
projectRouter,
subProjectRouter
}
说明:projectRouter为当前项目的router.config.json的对象形式。 subProjectRouter为包含所有子项目的router.config.json对象,例如如下配置子项目:
cml.config.merge({
subProject: ['cml-subproject']
})
compiler.getRouterConfig() 返回值结构如下:
{
projectRouter: {
"mode": "history",
"domain": "https://www.chameleon.com",
"routes":[
{
"url": "/cml/h5/index",
"path": "/pages/page1/page1",
"name": "主项目",
"mock": "index.php"
}
]
},
subProjectRouter: {
// 子项目npm包名称为key,value为子项目的router.config.json
"cml-subproject": {
"mode": "history",
"domain": "https://www.chameleon.com",
"routes":[
{
"url": "/cml/h5/index",
"path": "/pages/page1/page1",
"name": "主项目",
"mock": "index.php"
}
]
}
}
}
利用这个方法可以获取路由配置,用户可以根据这些配置进行路由实现,同时app节点的dependencies
字段中的节点都是页面节点。
组件化相关信息
1 cml文件的extra字段 (0.4.0 以上开始生效) CMLNode中的extra字段用于存放节点的额外信息,cml文件对应的节点,extra中的componentFiles字段记录cml文件引用的组件信息,结构如下:
{
componentFiles: {
demo-com: "/user/cml/demo-project/src/components/demo-com/demo-com.cml"
}
}
key为组件名称,value为组件的绝对路径。
2 cml文件节点的dependencies字段
cml文件节点的dependencies字段记录的就是这个cml文件引用的组件节点,其中如果cml文件是app节点 则dependencies
中还包含页面节点。通过componentFiles
字段中的组件绝对路径匹配dependencies
中节点的realPath
字段,就能找到组件名对应的节点。
3 cml节点的json子节点 cml节点的children字段存放cml文件的四个子节点,其中moduleType为json的节点convert字段为编译后的json对象。
扩展新端demo仓库: https://github.com/chameleon-team/cml-extplatform-demo