是不是要先有一些准备工作才对
首先,这篇文章的目的是为了学习基本的 webpack 工作流程,来实现一个简单版的 react 单文件组件,就是类似于 vue 的那种,不过我这边打算魔改的更厉害一点。
而且这种项目本身就没有什么实际的工程价值(大概),所以作为一个玩具性质的项目,希望可以让大家玩的更开心一点~
回到最开始的问题上来,如果我们要做这么一个单文件组件,需要用到那些知识呢?
1. 需要知道,源代码变为程序,大概总会走这么个流程(string -> tokens -> ast)
2. 随便自己喜欢,一个无关紧要但需要作为某种源代码标示的文件后缀(.rex)
3. 由于要对源码级别的文件进行操作,最好知道一些文件编码/命令行终端等一些常识(不知道也无所谓吧...)
4. 一定要知道,这个项目的目的是要把我们自定义的源文件变成一个什么东西(rex -> js/ts -> jsx/tsx)
5. 可以预计本项目会存在大量偷懒技巧(主要是感觉没必要 = =)
好啦,该知道的应该都已经知道了(不知道的现在也应该已经知道了),接下来可以正式的开始搞这么一个东西了
PS: 写这个的时候,虽然已经做了一部分调研,但不能完全保证可以按照预期将项目实现(只有 99% 的自信~,反正实现不出来我就删文章~)
自定义一种通用性比较好又容易被解析的语法
经过我大量实验(踩坑),语法定义方面大概有这么几点要求:
1. 通用性好(主要是懒)
2. 容易解析(还是懒~)
3. 不能和目标语言有语法冲突
除了第三点之外,完全可以按个人喜好来进行绝对自由的自定义。 PS:如果是实际的项目,严格遵循目标语言的语法会有更高的实用价值(但这样会少了一部分可以用来玩的东西 T T),但如果目标语言的某些语法特性本身就很让人不爽,这里就可以想办法替换掉它。
所以,最终的我定义出来了这么一个玩意
@[MODULE(arg1 = 'value', arg2 = 1)] |> pipe1 |> pipe2
// target code
全量语法就是这么个东西了,挤点就挤点吧,反正有空格的地方都可以换行 orz
最终目标(stage1)是可以完整解析下面这种格式的语法并且可以正常运行~
import * as React from 'react'
import * as Rex from 'rex.macro'
// js code
const [props, component] = Rex.generate()
export { component as TestComponent }
@[COMPONENT]
const [count, setCount] = React.useState(0)
const cx = Rex.useStyles()
@[RENDER] |> Rex.if(count === 2)
<div className={cx('container')}>
<div className={cx('title')}>count is 2</div>
<div>count: {count}</div>
<div onClick={setCount(x => x + 1)}>add count</div>
</div>
@[RENDER]
<div className={cx('container')}>
<div className={cx('title', { red: count === 3 })}>title text</div>
<div>count: {count}</div>
<div onClick={setCount(x => x + 1)}>add count</div>
</div>
@[STYLE(lang = 'scss')]
.container {
color: #bdbdbd;
width: "${count * 10}px";
}
.title {
color: blue;
&.red {
color: red;
}
}
最终目标就是这个样子啦,如果一切顺利的化,应该可以成功引入这种文件并且正确执行~
自定义语法时候的心路历程(如果太长可以不看)
又要把这个东西拿出来了,也是第一个步骤中首先要解析的东西
@[MODULE(arg1 = 'value', arg2 = 1)] |> pipe1 |> pipe2
其实最开始的时候并没有想着要搞这么复杂(虽然也没有很复杂 = =),只是想很简单的拆出来 react 组件中比较常规的部分,顺便强迫症一样的减少某些嵌套层级。
在目前这个时间节点,一个典型的 react 组件写起来大概会是这么个样子
import styles from './test.module.scss'
// js code
const TestComponent = props => {
const [count, setCount] = React.useState(0)
return (
<div></div>
)
}
虽然这种写法虽然已经很舒服了,但如果是为了更好的阅读,以及可以方便的拆分组件的粒度(完全的个人想法),所以就很自然的想出来了这么个东西
// js code
#COMPONENT
const [count, setCount] = React.useState(0)
#RENDER
<div></div>
#STYLE
.test-name {
color: red;
}
很简单的将代码分成三段,然后再组合成一个正经的 react 组件,顺便可以补上 export default
从结果上看,只需要根据关键字去拆分为不同部分的源码,然后再将想方设法糅合成下面这个样子
// js code
export default function() {
const [count, setCount] = React.useState(0)
return (
<React.Fragment>
<div></div>
<style jsx>`
.test-name {
color: red;
}
`</style>
</React.Fragment>
)
}
就以及可以完美制作一个如标题所示的单文件组件了,而且实际价值上或许会更高一点,因为 styled-jsx 已经是一个比较成熟的方案了,而且由于改动很小,测试方面的成本也很低 = =
但是呢,如果是这样实现的话,这个项目的乐趣就少了...大概 90% 左右吧,为了更好的贯彻娱乐精神,所以之后可能会手撸一个 css in js 的方案(手动斜眼
PS: 主要原因还是因为调研了现有的 css in js 方案之后,并没有发现比较符合期望的解决方案,而且目前让我最满意的方案就是 css-modules,所以我这里的想法就是将 css-modules 进行简单魔改后内置进单文件组件中
PS2: 如果有合适的方案我肯定是懒得搞这样一个实现的(我不管,我就是没有找到)
心路历程吐槽完毕,接下来应该要正经的去做一些实现了(笑
演示一下 webpack 最基本的食用方案
声明一下标题没有打错( 再次声明一下上面一行最后的括号也没有打错( 禁止套娃啊喂!
在正确的使(食)用 webpack 之前,首先要确保自己安装了 node/npm 并且全局安装了 yarn(个人习惯,可以自行替换为 npm 指令),并且知道应该在哪里使用命令行指令
如果连这些常识问题都不知道的话,大佬你是怎么坚持并且看到现在的 orz(虽然我感觉一个正常的前端应该都知道这些东西,但万一完全不懂前端的人点进来了肿么办 = =)
(又水了几十个字)
接下来要废话很多的正式开始啦~
首先,需要找个自己喜欢的位置打开你的命令行终端,然后,执行
mkdir webpack-test
cd webpack-test
npm init
# 狂按回车
yarn add webpack webpack-cli
总的来说就是新建一个目录,然后在这个目录下做 webpack 的初始化~
然后在目录下新建一个 webpack.config.js
const path = require('path');
module.exports = {
// mode: 'development',
entry: './src/index.js', // 文件入口
output:{
path: path.resolve(__dirname, 'build'), // 输出路径
filename: 'bundle.js', // 输出的文件名
libraryTarget: 'umd' // 打包格式,这种是可以被更多环境引用的包,不用纠结这个 = =
},
};
这应该是一个没有任何多余代码的 webpack.config.js 了吧
之后新建 /src/index.js 对应上述的文件入口
修改 package.json, 在 scripts 下增加脚本"build": "webpack"
经过上述操作之后,应该会得到这么一坨东西
- 目录结构

webpack.config.js

package.json

现在的 src/index.js 还只是一个空文件 = =
在正式实现 react 单文件组件这么一个巨大的工程之前,或许应该先定义一个小目标,而在这之前,需要先知道 webpack 帮我们做了什么事情,我们可以用 webpack 来做什么(手动斜眼
首先,webpack 从入口开始,找到所有被引用的 js 源码,将其抽离并打包成为一个 js 文件,在这之中也会做一些语法优化之类的东西,显然暂时并不需要关心这个 = =
一个很重要但又比较容易忽略的点是,webpack 的输入和输出一定是一个 js 文件
在想做一些骚操作的时候,可以先去想想这句话,重点是 输入和输出, 一个 和 js 文件,重要的事情我要说两遍
当遇到不符合这种场景的问题的时候,正常情况下应该就需要引入 webpack-plugin 或者对应的 loader plugin 来实现相关功能,这个先暂时忽略
对于一个最纯真的 webpack 项目而言,现在可以做的测试就是
在 src 下新建 temp.js 写入以下内容
export function temp() {
console.log('temp')
}
在 src/index.js 中写入以下内容
import { temp } from './temp'
temp()
使用 webpack 一个最显而易见的好处就是帮我们实现了 es6 的模块语法,大赞
之后在命令行终端运行 npm run build,wepback 就会生成一个 bulid/bundle.js 的文件,输出的目录和文件名就是之前在 webpack.config.js 中配置的内容
然后可以看看 build/bundle.js 中的这一坨东西,这一坨东西的功能就是输出一行 console.log('temp'),我只想简简单单的做一行输出为什么要搞这么麻烦啊喂
可以在浏览器下执行这么一坨东西来看看效果,大概是这么个样子

虽然已经是混淆过的东西,但最后这么一行 console.log('temp') 作为程序的本体还是那么显眼 orz
还记得 webpack.config.js 中有一行被注释的代码么,不记得的话就赶快回去看!
就是这个 mode: 'development' 开启 development 模式可以让打包出来的结果稍微那么一点点适合人类阅读,重新 npm run build 之后 bundle.js 就会变成这个样子

大多数时候只需要关心文件最末尾的部分就行了(不,实际上大多数时候我们根本不需要关心这个)(笑
正式开始最开始说的那个小目标
好啦好啦,好像跑题有点远,但这个真的是很重要的东西,废话之后继续开始之前制定一个小目标的计划~
这个小目标就是~
首先将 temp.js 的后缀修改为 .rex
修改 index.js 中的 import { temp } from './temp' -> import { temp } from './temp.rex',然后运行 npm run build ~
哇~竟然没有报错,当然 build 的结果也是和之前没有任何区别的
可以推测,webpack 把不认识的文件都会当作 js 文件去处理,如果遇到了 js 不认识的语法就会报错(我瞎猜的 orz
还记得刚刚提到的 很重要但又很容易忽略 的点吗?webpack 的输入和输出一定是 一个 js 文件
所以对于 webpack 不认识的文件而言,需要提供一种工具,将其转换为可以被识别的 js,而这个工具就是 webpack loader~
这里可以先修改 temp.rex 变为一下内容
function getText() {
return 'test text'
}
#RENDER
<div>getText()</div>
这个时候如果去运行一下 npm run build 就会出现意料之中的报错啦~

可以很明显的看出,这个报错是无法识别 #RENDER 这种语法,而且也非常亲切的告诉你,可以对这种文件类型增加 loader,甚至还指出了 loader 的具体使用方式的文档地址
所以这个时候,就需要为这种自定义的文件增加一个 loader 了
在根目录下新建文件 loader/rex-loader.js 并写入一下内容
module.exports = function(source) {
console.log('source:', source)
return source;
};
webpack.config.js 也需要做一些修改来引入这个 loader,修改完成后会变成这个样子
const path = require('path');
module.exports = {
// mode: 'development',
entry: './src/index.js', // 文件入口
output:{
path: path.resolve(__dirname, 'build'), // 输出路径
filename: 'bundle.js', // 输出的文件名
libraryTarget: 'umd' // 打包格式,这种是可以被更多环境引用的包,不用纠结这个 = =
},
module:{
rules:[
{
test:/\.rex$/,
use: './loader/rex-loader.js'
}
]
},
};
依旧还是木有一行多余代码~
这个时候运行 npm run build,就可以在终端看到输出的源代码了,因为匹配了 .rex 的文件,所以其它的 js 文件并不会进入这个 loader

输出在报错上面,找不到的话可以向上翻一翻~
对,就是这个报错,现在还木有消失,现在可以先暂时用一种极为简单的方法解决掉报错
module.exports = function(source) {
console.log('source:', source)
return source.split('#RENDER')[0];
};
这个时候 npm run build,就可以发现报错已经消失了,原因是因为简单粗暴的丢掉了 .rex 文件中非 js 代码的部分
可以很容易发现 loader 的一些作用,loader 就是用来将非 js 代码处理为 js 代码的
所以这个时候,第一个小目标就可以确定下来了,就是将 webpack 编译后的代码,放到浏览器中运行,可以在底部生成一个自定义的 html 标签~
对,就是这么简单的一个小目标
不过首先要知道 html 语法也是不被 js 识别的,所以除了要处理自定义的 #RENDER 之外,还要额外去简单的处理 html 标签
可以预期的是,如果使用 js 创建标签,大概是下面这个样子
const node = document.createElement('div')
node.appendChild(document.createTextNode('text'))
document.body.appendChild(node)
浏览器下直接执行这段代码,就可以在 body 最后增加一个标签了,可以很容易的想出来,我们需要将 temp.rex 文件中的内容,转化为这个样子,之前已经测试过了,即使走了 webpack 编译,也不会影响代码的实际效果
这个时候,就可以凭借自己的喜好来实现这么一个功能,一切从简的结果
module.exports = function(source) {
const [js, code] = source.split('#RENDER')
const tag = code.match(/<.*?>/)[0].slice(1, -1)
const text = code.match(/>.*</)[0].slice(1, -1)
const parseCode = `
const node = document.createElement('${tag}')
node.appendChild(document.createTextNode(${text}))
export default node
`
return js + parseCode;
};
要记住在 loader 中拿到的源码只是普通的字符串,返回给 webpack 的代码也只是普通的字符串,所以这个时候就当是在自己在写代码就行了,只不过写的代码需要用引号装起来
这里的算法,emmmmmm 实际上就是在开玩笑,对于 html 标签的解析部分,除了可以改标签名和里面的文字之外,别的什么都做不到,标签中的内容也不是正经的 html(只做示意用,各位大佬不要在意细节 orz
现在回到我们的 index.js 上,由于这里是做了一个默认导出,所以对文件作出以下修改
import node from './temp.rex'
document.body.appendChild(node)
应该是一个很容易理解的逻辑,导如一个组件,把它添加进入 body~
执行 npm run build 可以得到一堆奇怪的代码,将这一堆奇怪的代码放在浏览器下运行,就会发现 body 下面多了一行 div



这个 div 就是我们刚刚 .rex 文件里面的样子
第一篇章结束,一个简单的小目标已经完成啦~
撒花~