根本没什么人期待的第三篇章来啦~
前两篇链接 orz
解决一下之前遗留的一个大坑
在上一篇章中,对于目前的单文件组件还遗留了大概三个左右的问题,这次就来解决其中的一个:直接操作源码太过危险
虽然时间可能有些久远了,但小伙伴们应该还能依稀记得某个略微有些奇怪的自定义语法 @[MODULE(arg1 = 'value', arg2 = 1)] |> pipe1 |> pipe2
是时候解释一下为什么要定义成这种奇怪的样子啦~
首先最开始的 @ 符号,这个并没有什么特殊的意义,可能只是个人爱好/避免语法冲突
然后就是这样一个东东 [MODULE(agr1 = 'value')],这一整个玩意基本上就是直接照搬了 c# attribute 的语法规则,意外的我还蛮喜欢这个东西的,然后在这个项目中,这个规则和前面的 @ 强绑定在了一起,是自定义语法规则的第一部分~
第二部分就是一个当前时间节点还在提案中的未来语法,管道操作符 |>,所以第二部分的作用就是可以对第一部分进行虚拟的管道操作,感官上还是比较不错的~
第三部分就是被这两部分语法分割开的普通 js 代码,虽然之前的实现是有奇怪的东西混进来了,这次要做的事情就是把混进来的东西干掉(手动斜眼
所以呢?为什么要分成三个部分呢?
回到最开始的那个问题上来,我们要做的事情,其实就是一个 string -> js string 的一个过程,但是,之前由于工具链的限制(只有 loader),所以并不能完全将非 js 代码抽离出来(其实想做也没什么不可以的,但成本太高 = =
所以在最开始的抽象定义中,直接就将语法抽象成了 自定义语法 -> 类 js 语法 -> js 语法 三个部分,因为是完全解藕的设计,所以扩展性和维护性会好一些 orz
什么?你说可以在 loader 中继续做扩展?醒醒吧!仔细想想 loader 里面做了一些什么奇怪的事情吧,直接替换源码字符串这种操作,从来都不是一件很安全的事情
而且 loader 里面对于 js 部分的操作完全无力,总不能在 loader 中手写 AST 解析器吧 orz
所以,就需要引入另外一个工具来基于 js 语法进行魔改,对就是那个经常见到听到但可能不知道具体做什么 babel
这次的小目标是什么呢?
单独把之前有问题的两个语法点拎出来说
...
@[COMPONENT]
const props = @{RexProps}
...
@[RENDER] |> count === 2
...
就是这个看不出来是什么奇怪语法的东西混进去了,从进度上看这次依旧用不到 (arg1 = 'value') 这部分的自定义语法,原则上上面说的第一部分的工作在 loader 中处理,其余部分在 babel 中处理,因为 |> 操作符还在提案,所以就现在 loader 中实现这样一个操作符~
好的呢?这次只改动这么一丢丢的东西,将上面语法中非 js 语法的部分转换一下,就像这样
import { rexConstructor, rexIf } from 'remix.macro'
...
const [props] = rexConstructor()
@[COMPONENT]
...
@[RENDER] |> rexIf(count === 2)
...
虽然看起来变化并不是很大,但这样就可以很好的支持 js 相关的一些特性,比如说,虽然没有做什么额外的工作,但当这个小目标完成之后,component 就可以很好的支持 hoc 语法了呢!
稍微说一下 rexConstructor 这个东东,为什么看起来有点奇怪,因为想要一个方法同时支持 export 和 props 的获取,万一以后想支持 ts 了怎么办,总要有点梦想是不是(不过这么写 props 的作用域会有点问题,不过先这样吧 orz
这次要做的是两个语法点的转换
const [props, comp] = rexConstructor()
function __REX_COMPONENT__() {
...
}
⬇️
function comp(props) {
...
}
// 另外一个
function comp() {
rexIf(...)(...)
}
⬇️
function comp() {
if (...) {
return ...
}
}
这次的小目标就是这个啦,看上去非常简单的样纸呢~
一个很好玩的玩具 babel macro
因为 babel 所以才有现在的前端(个人主观感觉 orz)所以要感谢 babel 的诞生可以让我们可以用到一些奇怪的语法特性
然后让我们正式开始~
首先,依旧是例行公事的脚本运行/创建项目,项目的搭建直接参考了这个项目(抱歉我太菜了 orz github.com/kentcdodds/…
mkdir remix-macro
cd remix-macro
npm init // 狂按回车
yarn add babel-macros babel-plugin-macros babel-plugin-tester jest --dev
然后就可以开始快乐的写代码了~
等等,是不是漏了什么?由于 babel macro 的某些流程比较复杂,所以之前参考之前上面那个链接项目的代码就好,或者直接来参考我这个(这玩意以后有可能会更新 github.com/frontend-ki…
然后 babel 这个东西,稍稍还是有点麻烦的,官方文档肯定比我说的要好,所以附上链接 = = github.com/jamiebuilds…
zh-cn 手册在这里 github.com/jamiebuilds…
文档里有一个 AST Explorer 的链接,不过容易漏掉,这里就拉出来了 astexplorer.net/
吐槽一下这次粘的链接好多(粘这么多链接还写什么写,哼
为了一切从简,所以这个项目也只做最核心的部分了 orz
新建 lib/index.macro.js 写入以下内容,然后 npm run test,注意啦~这次是 test
const { createMacro } = require('babel-plugin-macros')
module.exports = createMacro(({ references, state, babel }) => {
const { rexIf = [], rexConstructor = [] } = references
rexIf.forEach(path => {
const funPath = path.getFunctionParent()
// 判断调用环境是否在 function 中
if (!(babel.types.isFunctionDeclaration(funPath) ||
babel.types.isArrowFunctionExpression(funPath))) {
// 如果不是在 function 中调用,则删除此节点
path.getStatementParent().remove()
return
}
// 生成目标 AST 的对应结构并替换当前节点
path.getStatementParent().replaceWith(babel.types.ifStatement(
path.parent.arguments[0],
babel.types.blockStatement([
babel.types.returnStatement(
path.parentPath.parent.arguments[0]
)
])
))
})
rexConstructor.forEach(path => {
const funPath = path.getFunctionParent()
// 判断调用环境是否在 program 中
if (!babel.types.isProgram(funPath)) {
// 如果不是在 program 中调用,则删除此节点
path.getStatementParent().remove()
return
}
const paramNode = path.parentPath.parentPath.get('id').node
// 暂时只支持结构赋值的方式取 props
if (!babel.types.isArrayPattern(paramNode)) {
return
}
const [propsNode, funNode] = paramNode.elements
// 替换 fun 中的内容
funPath.traverse({
FunctionDeclaration(fpath) {
// 如果根节点的 fun 是自定义标示的 __REX_COMPONENT__,则进行替换
if (fpath.get('id').node.name === '__REX_COMPONENT__') {
if (funNode) {
fpath.get('id').node.name = funNode.name
}
if (propsNode) {
fpath.node.params = [propsNode]
}
}
}
})
path.getStatementParent().remove()
})
})
对,babel macro 对文件名/项目名都有一些限制,这个就没有什么办法了,大佬们好好看文档 orz
和之前 loader 的算法一样,babel 这部分的说明我也十分无能为力,再说我写的也很烂呀
核心代码只有这么一点点,非常非常的少,大致思路就是,通过变化 AST 的结构,使得生成的 js 代码变成自己想要的形状
然后简单的 npm 发布一下,babel macro 的部分就实现啦~
PS:babel 有点难 = = PS2:确实是有点难,也很麻烦 orz PS?: 好像也么有那么难,不过确实很麻烦(笑
babel plugin/babel macro 的开发应该是有那么一点难度的,这里只是做了两个非常非常简单的 macro,如果想认真去研究 AST 估计只能去看文档了,这篇文章主要目的是浏览 webpack 的基本工作流程,所以 babel 的部分就只提供方案了 orz
不要忘记陪伴过我们一段时间的 loader
或许还能想起来大明湖畔一个被 reject 过的项目,由于 loader 项目没有做 test 相关的搭建,还是在之前那个旧项目上继续改造吧 orz
需要先脚本运行一下 yarn add remix.macro@0.0.2 记得要锁版本,因为这个是我发上去的,说不定什么时候版本号就会更新了,而且很荣幸的这个版本号变成了 0.0.2
由于 rexConstructor 不明原因的不符合预期,临时补一个 rexProps 的 macro,先不考虑 ts 的支持问题了 orz
rexProps.forEach(path => {
const funPath = path.getFunctionParent()
// 判断调用环境是否在 function 中
if (!(babel.types.isFunctionDeclaration(funPath) ||
babel.types.isArrowFunctionExpression(funPath))) {
// 如果不是在 function 中调用,则删除此节点
path.getStatementParent().remove()
return
}
funPath.node.params = [path.parentPath.parent.id]
path.getStatementParent().remove()
})
然后放出当前项目的 diff,实际上主要是 loader 的改动
diff --git a/loader/remix-loader.js b/loader/remix-loader.js
index c9332e0..7ec656c 100644
--- a/loader/remix-loader.js
+++ b/loader/remix-loader.js
@@ -60,8 +60,7 @@ function getTokens(input) {
}
let deep = 0
// 匹配管道操作符
- // 因为这里只匹配表达式,所以匹配做了简单修改 \s -> \n
- while((deep || !char.match(/\n/)) && cur < input.length) {
+ while((deep || !char.match(/\s/)) && cur < input.length) {
str += char
// 判断括号是否匹配完成
if (char.match(/\(|\[|\{/)) {
@@ -82,36 +81,51 @@ function getTokens(input) {
return tokens
}
-const REX_PROPS = '__REX_PROPS__'
+const REX_COMPONENT = '__REX_COMPONENT__'
const REX_STYLES = '__REX_STYLES__'
function parseTokens(tokens) {
let component = ''
+ let componentPipes = []
let renders = ''
let source = ''
let styles = ''
+ let hasDefaultRender = false
+ let hasDefaultExport = true
for (const token of tokens) {
if (token.type === 'source') {
// 设置 source code
source += token.value
+ if (token.value.includes('export default')) {
+ hasDefaultExport = false
+ }
}
if (token.type === 'component') {
- // 设置 component code, 并且替换 @{RexProps}
- component = token.value.replace('@{RexProps}', REX_PROPS)
+ // 设置 component code
+ component = token.value
+ // 获取 component 设置的 pipes
+ componentPipes = token.pipes
}
if (token.type === 'render') {
- // 设置 render code
- let render = `return (
+ // 设置基本的 render code
+ let render = `
<React.Fragment>
${token.value}
<style jsx>{\`${REX_STYLES}\`}</style>
</React.Fragment>
- )`
- if (token.pipes.length) {
- // 临时使用 pipes[0] 作为是否 render 的条件
- const condition = token.pipes[0]
- render = `if (${condition}) {${render}}`
+ `
+ // 判断是否设置了 pipes
+ if (token.pipes.length === 0) {
+ hasDefaultRender = true
+ // 如果没有 pipes 则默认直接 return render
+ render = `return (${render})`
+ } else {
+ // 解析并设置 render 的 pipes
+ for (const pipe of token.pipes) {
+ render = `${pipe}(${render})`
+ }
}
+ // 稍微使生成的代码好看一点点
render = `\n${render}\n`
renders += render
}
@@ -122,13 +136,18 @@ function parseTokens(tokens) {
}
// 全局替换 __REX_STYLES__ 占位符
renders = renders.replace(/__REX_STYLES__/g, styles)
- const func = `export default function (${REX_PROPS}) {
+
+ // 设置组件的 func 模版
+ const func = `function ${REX_COMPONENT}() {
${component}
- ${renders}
+ ${hasDefaultRender ? renders : 'return null'}
}`
- source += func
- return source
+ // 如果没有 export default,则设置默认导出为当前组件
+ if (hasDefaultExport) {
+ source += `\n export default ${REX_COMPONENT}\n`
+ }
+ return source + func
}
module.exports = function(source) {
diff --git a/package.json b/package.json
index 428455e..c6dcd2b 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
+ "remix.macro": "^0.0.2",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
diff --git a/src/test.rex b/src/test.rex
index 9269556..e14f0cb 100644
--- a/src/test.rex
+++ b/src/test.rex
@@ -1,15 +1,15 @@
import React from 'react'
+import { rexProps, rexIf } from 'remix.macro'
@[COMPONENT]
-
-const props = @{RexProps}
+const props = rexProps()
const [count, setCount] = React.useState(0)
const onAddCount = () => {
setCount(x => x + 1)
}
-@[RENDER] |> count === 2
+@[RENDER] |> rexIf(count === 2)
<div className="name">render by condition</div>
<div>count: {count}</div>
把 loader 的代码 copy 到之前那个用于发布的项目,然后发布~
其实这样做并不好,最好还是能搭建一下相关 test 流程
最后回到那个被 react-app-rewired 调教过的项目上去,在真实的项目中演练一下之前的成果,脚本运行 yarn add remix.macro@0.0.2 remix-loader@0.0.3
之后将 temp.rex 替换为 remix.macro 版本,就是这么简单的步骤,大功告成~
放出这次改动的 diff
diff --git a/package.json b/package.json
index 067b251..41d7913 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
"react-app-rewired": "^2.1.6",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
- "remix-loader": "0.0.2",
+ "remix-loader": "0.0.3",
+ "remix.macro": "0.0.2",
"styled-jsx": "^3.3.0"
},
"scripts": {
diff --git a/src/temp.rex b/src/temp.rex
index 9269556..85da0cb 100644
--- a/src/temp.rex
+++ b/src/temp.rex
@@ -1,15 +1,16 @@
import React from 'react'
+import { rexProps, rexIf } from 'remix.macro'
@[COMPONENT]
-const props = @{RexProps}
+const props = rexProps()
const [count, setCount] = React.useState(0)
const onAddCount = () => {
setCount(x => x + 1)
}
-@[RENDER] |> count === 2
+@[RENDER] |> rexIf(count === 2)
<div className="name">render by condition</div>
<div>count: {count}</div>
好啦,完结撒花~~~
虽然这次改动看起来并不大,但这次的目的主要是为了解耦还有提升扩展性,接下来就可以在这个基础上按照自己的喜好随意揉捏了(笑
好啦好啦,到现在为止这个单文件组件已经基本上完成啦,剩下主要就是功能方面的扩展 stage12345 ... 有生之年系列 orz
什么?说好的手撸一个 css-in-js 方案呢?
肯定是在下期啦~如果还有的话,也有可能真的没有了 orz
再次完结撒花~~~