内容简介:babel是一个非常强大的工具,作用远不止我们平时的ES6 -> ES5语法转换这么单一。在前端进阶的道路上,了解与学习babel及其灵活的插件模式将会为前端赋予更多的可能性。本文就是运用babel,通过编写babel插件解决了一个实际项目中的问题。本文相关代码已托管至github:
babel是一个非常强大的工具,作用远不止我们平时的ES6 -> ES5语法转换这么单一。在前端进阶的道路上,了解与学习babel及其灵活的插件模式将会为前端赋予更多的可能性。
本文就是运用babel,通过编写babel插件解决了一个实际项目中的问题。
本文相关代码已托管至github: babel-plugin-import-customized-require
1. 遇到的问题
最近在项目中遇到这样一个问题:我们知道,使用webpack作为构建 工具 是会默认自动帮我们进行依赖构建;但是在项目代码中,有一部分的依赖是运行时依赖/非编译期依赖(可以理解为像requirejs、seajs那样的纯前端模块化),对于这种依赖不做处理会导致webpack编译出错。
为什么需要非编译期依赖呢?例如,在当前的业务模块(一个独立的webpack代码仓库)里,我依赖了一个公共业务模块的打点代码
// 这是home业务模块代码 // 依赖了common业务模块的代码 import log from 'common:util/log.js' log('act-1'); 复制代码
然而,可能是由于技术栈不统一,或是因为common业务代码遗留问题无法重构,或者仅仅是为了业务模块的分治……总之,无法在webpack编译期解决这部分模块依赖,而是需要放在前端运行时框架解决。
为了解决webpack编译期无法解析这种模块依赖的问题,可以给这种非编译期依赖引入新的语法,例如下面这样:
// __my_require__是我们自定义的前端require方法 var log = __my_require__('common:util/log.js') log('act-1'); 复制代码
但这样就导致了我们代码形式的分裂,拥抱规范让我们希望还是能够用ESM的标准语法来一视同仁。
我们还是希望能像下面这样写代码:
// 标准的ESM语法 import * as log from 'common:util/log.js'; log('act-1'); 复制代码
此外,也可以考虑使用webpack提供了externals配置来避免某些模块被webpack打包。然而,一个重要的问题是,在已有的common代码中有一套前端模块化语法,要将webpack编译出来的代码与已有模式融合存在一些问题。因此该方式也存在不足。
针对上面的描述,总结来说,我们的目的就是:
- 能够在代码中使用ESM语法,来进行非编译期分析的模块引用
- 由于webpack会尝试打包该依赖,需要不会在编译期出错
2. 解决思路
基于上面的目标,首先,我们需要有一种方式能够标识不需要编译的运行期依赖。例如 util/record
这个模块,如果是运行时依赖,可以参考标准语法,为模块名添加标识: runtime:util/record
。效果如下:
// 下面这两行是正常的编译期依赖 import React from 'react'; import Nav from './component/nav'; // 下面这两个模块,我们不希望webpack在编译期进行处理 import record from 'runtime:util/record'; import {Banner, List} from 'runtime:ui/layout/component'; 复制代码
其次,虽然标识已经可以让开发人员知道代码里哪些模块是webpack需要打包的依赖,哪些是非编译期依赖;但webpack不知道,它只会拿到模块源码,分析import语法拿到依赖,然后尝试加载依赖模块。但这时webpack傻眼了,因为像 runtime:util/record
这样的模块是运行时依赖,编译期找不到该模块。那么,就需要通过一种方式,让webpack“看不见”非编译期的依赖。
最后,拿到非编译期依赖,由于浏览器现在还不支持ESM的import语法,因此需要将它变为在前端运行时我们自定义的模块依赖语法。
3. 使用babel对源码进行分析
3.1. babel相关工具介绍
对babel以及插件机制不太了解的同学,可以先看这一部分做一个简单的了解。
babel是一个强大的javascript compiler,可以将源码通过词法分析与语法分析转换为AST(抽象语法树),通过对AST进行转换,可以修改源码,最后再将修改后的AST转换会目标代码。
由于篇幅限制,本文不会对compiler或者AST进行过多介绍,但是如果你学过编译原理,那么对词法分析、语法分析、token、AST应该都不会陌生。即使没了解过也没有关系,你可以粗略的理解为:babel是一个compiler,它可以将javascript源码转化为一种特殊的数据结构,这种数据结构就是树,也就是AST,它是一种能够很好表示源码的结构。babel的AST是基于 ESTree 的。
例如, var alienzhou = 'happy'
这条语句,经过babel处理后它的AST大概是下面这样的
{ type: 'VariableDeclaration', kind: 'var', // ...其他属性 decolarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: 'alienzhou', // ...其他属性 }, init: { type: 'StringLiteral', value: 'happy', // ...其他属性 } }], } 复制代码
这部分AST node表示,这是一条变量声明的语句,使用 var
关键字,其中id和init属性又是两个AST node,分别是名称为alienzhou的标识符(Identifier)和值为happy的字符串字面量(StringLiteral)。
这里,简单介绍一些如何使用babel及其提供的一些库来进行AST的分析和修改。生成AST可以通过 babel-core
里的方法,例如:
const babel = require('babel-core'); const {ast} = babel.transform(`var alienzhou = 'happy'`); 复制代码
然后遍历AST,找到特定的节点进行修改即可。babel也为我们提供了traverse方法来遍历AST:
const traverse = require('babel-traverse').default; 复制代码
在babel中访问AST node使用的是vistor模式,可以像下面这样指定AST node type来访问所需的AST node:
traverse(ast, { StringLiteral(path) { console.log(path.node.value) // ... } }) 复制代码
这样就可以得到所有的字符串字面量,当然你也可以替换这个节点的内容:
let visitor = { StringLiteral(path) { console.log(path.node.value) path.replaceWith( t.stringLiteral('excited'); ) } }; traverse(ast, visitor); 复制代码
注意,AST是一个mutable对象,所有的节点操作都会在原AST上进行修改。
这篇文章不会详细介绍babel-core、babel-traverse的API,而是帮助没有接触过的朋友快速理解它们,具体的使用方式可以参考相关文档。
由于大部分的webpack项目都会在loader中使用babel,因此只需要提供一个babel的插件来处理非编译期依赖语法即可。而babel插件其实就是导出一个方法,该方法会返回我们上面提到的visitor对象。
那么接下来我们专注于visitor的编写即可。
3.2 编写一个babel插件来解决非编译期依赖
ESM的import语法在AST node type中是 ImportDeclaration :
export default function () { return { ImportDeclaration: { enter(path) { // ... } exit(path) { let source = path.node.source; if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) { // ... } } } } } 复制代码
在enter方法里,需要收集ImportDeclaration语法的相关信息;在exit方法里,判断当前ImportDeclaration是否为非编译期依赖,如果是则进行语法转换。
收集ImportDeclaration语法相关信息需要注意,对于不同的import specifier类型,需要不同的分析方式,下面列举了这五种import:
import util from 'runtime:util'; import * as util from 'runtime:util'; import {util} from 'runtime:util'; import {util as u} from 'runtime:util'; import 'runtime:util'; 复制代码
对应了三类specifier:
- ImportSpecifier:
import {util} from 'runtime:util'
,import {util as u} from 'runtime:util';
- ImportDefaultSpecifier:
import util from 'runtime:util'
- ImportNamespaceSpecifier:
import * as util from 'runtime:util'
import 'runtime:util'
中没有specifier
可以在ImportDeclaration的基础上,对子节点进行traverse,这里新建了一个visitor用来访问Specifier,针对不同语法进行收集:
const specifierVisitor = { ImportNamespaceSpecifier(_path) { let data = { type: 'NAMESPACE', local: _path.node.local.name }; this.specifiers.push(data); }, ImportSpecifier(_path) { let data = { type: 'COMMON', local: _path.node.local.name, imported: _path.node.imported ? _path.node.imported.name : null }; this.specifiers.push(data); }, ImportDefaultSpecifier(_path) { let data = { type: 'DEFAULT', local: _path.node.local.name }; this.specifiers.push(data); } } 复制代码
在ImportDeclaration中使用specifierVisitor进行遍历:
export default function () { // store the specifiers in one importDeclaration let specifiers = []; return { ImportDeclaration: { enter(path) { path.traverse(specifierVisitor, { specifiers }); } exit(path) { let source = path.node.source; if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) { // ... } } } } } 复制代码
到目前为止,我们在进入ImportDeclaration节点时,收集了import语句相关信息,在退出节点时,通过判断可以知道目前节点是否是非编译期依赖。因此,如果是非编译期依赖,只需要根据收集到的信息替换节点语法即可。
生成新节点可以使用babel-types。不过推荐使用babel-template,会令代码更简便与清晰。下面这个方法,会根据不同的import信息,生成不同的运行时代码,其中假定__my_require__方法就是自定义的前端模块require方法。
const template = require('babel-template'); function constructRequireModule({ local, type, imported, moduleName }) { /* using template instead of origin type functions */ const namespaceTemplate = template(` var LOCAL = __my_require__(MODULE_NAME); `); const commonTemplate = template(` var LOCAL = __my_require__(MODULE_NAME)[IMPORTED]; `); const defaultTemplate = template(` var LOCAL = __my_require__(MODULE_NAME)['default']; `); const sideTemplate = template(` __my_require__(MODULE_NAME); `); /* ********************************************** */ let declaration; switch (type) { case 'NAMESPACE': declaration = namespaceTemplate({ LOCAL: t.identifier(local), MODULE_NAME: t.stringLiteral(moduleName) }); break; case 'COMMON': imported = imported || local; declaration = commonTemplate({ LOCAL: t.identifier(local), MODULE_NAME: t.stringLiteral(moduleName), IMPORTED: t.stringLiteral(imported) }); break; case 'DEFAULT': declaration = defaultTemplate({ LOCAL: t.identifier(local), MODULE_NAME: t.stringLiteral(moduleName) }); break; case 'SIDE': declaration = sideTemplate({ MODULE_NAME: t.stringLiteral(moduleName) }) default: break; } return declaration; } 复制代码
最后整合到一开始的visitor中:
export default function () { // store the specifiers in one importDeclaration let specifiers = []; return { ImportDeclaration: { enter(path) { path.traverse(specifierVisitor, { specifiers }); } exit(path) { let source = path.node.source; let moduleName = path.node.source.value; if (t.isStringLiteral(source) && /^runtime:/.test(source.value)) { let nodes; if (specifiers.length === 0) { nodes = constructRequireModule({ moduleName, type: 'SIDE' }); nodes = [nodes] } else { nodes = specifiers.map(constructRequireModule); } path.replaceWithMultiple(nodes); } specifiers = []; } } } } 复制代码
那么,对于一段 import util from 'runtime:util'
的源码,在该babel插件修改后变为了 var util = require('runtime:util')['default']
,该代码也会被webpack直接输出。
这样,通过babel插件,我们就完成了文章最一开始的目标。
4. 处理dynamic import
细心的读者肯定会发现了,我们在上面只解决了静态import的问题,那么像下面这样的动态import不是仍然会有以上的问题么?
import('runtime:util').then(u => { u.record(1); }); 复制代码
是的,仍然会有问题。因此,进一步我们还需要处理动态import的语法。要做的就是在visitor中添加一个新的node type:
{ Import: { enter(path) { let callNode = path.parentPath.node; let nameNode = callNode.arguments && callNode.arguments[0] ? callNode.arguments[0] : null; if (t.isCallExpression(callNode) && t.isStringLiteral(nameNode) && /^runtime:/.test(nameNode.value) ) { let args = callNode.arguments; path.parentPath.replaceWith( t.callExpression( t.memberExpression( t.identifier('__my_require__'), t.identifier('async'), false), args )); } } } } 复制代码
这时,上面的动态import代码就会被替换为:
__my_require__.async('runtime:util').then(u => { u.record(1); }); 复制代码
非常方便吧。
5. 写在最后
本文相关代码已托管至github: babel-plugin-import-customized-require
本文是从一个关于webpack编译期的需求出发,应用babel来使代码中部分模块依赖不在webpack编译期进行处理。其实从中可以看出,babel给我们赋予了极大的可能性。
文中解决的问题只是一个小需求,也许你会有更不错的解决方案;然而这里更多的是展示了babel的灵活、强大,它给前端带来的更多的空间与可能性,在许多衍生的领域也都能发现它的身影。希望本文能成为一个引子,为你拓展解决问题的另一条思路。
参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- XenServer编译内核模块
- XenServer编译内核模块
- 试读angular源码第四章:angular模块及JIT编译模块
- 编译Kettle的PDI DB Dialog模块失败
- 为NGINX和NGINX Plus编译第三方动态模块
- 模块化-Application多module共存编译生成解决方案
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网络多人游戏架构与编程
格雷泽 (Joshua Glazer)、马达夫 (Sanjay Madhav) / 王晓慧、张国鑫 / 人民邮电出版社 / 2017-10-1 / CNY 109.00
本书是一本深入探讨关于网络多人游戏编程的图书。 全书分为13章,从网络游戏的基本概念、互联网、伯克利套接字、对象序列化、对象复制、网络拓扑和游戏案例、延迟、抖动和可靠性、改进的延迟处理、可扩展性、安全性、真实世界的引擎、玩家服务、云托管专用服务器等方面深入介绍了网络多人游戏开发的知识,既全面又详尽地剖析了众多核心概念。 本书的多数示例基于C++编写,适合对C++有一定了解的读者阅读。本......一起来看看 《网络多人游戏架构与编程》 这本书的介绍吧!