我造了一个迷你 react - 起步篇
动机
关于 react 的源码也研究了很久,也输出了很多关于 react 的文章。由于学过的东西不用很快就会忘,因此决定根据自己对 react 的理解造一个类 react 的轮子。不过既然是迷你版,那么当然只支持小部分 react 的特性:
- 为了减轻复杂度,仅支持同步渲染,不支持时间切片
- 仅支持函数式组件
- 支持部分 hooks(useState, useRef, useEffect, useLayoutEffect)
开发这个迷你版轮子,一方面是为了更好的理解 react,将学到的知识应用到实践中。一方面也是顺便了解一下 end-to-end 的组件开发配置/开发过程。由于是迷你版的 react,所以我将其命名为 Leact(LiteReact)。
目前整个项目已经完成预计的功能,代码地址:
- github: github.com/lishion/lea…
- npm: www.npmjs.com/package/@l1…
各位大佬有兴趣的可以点个 star。
技术选型
采用 typescript + webpack + pnpm + jest 作为整体的技术框架
项目配置
jsx 支持
由于这是一个类 react 框架,对 jsx 的支持是首先要处理的问题。我们知道 jsx 实际上是babel/preset-react
这个 loader 将标签转换为 js 代码。对于某个标签,babel 会将其转换为:
React.createElemet(type, props, children)
因此,在我编写的框架中需要将其替换为 Leact.createElemet(type, props, children)
。对于这种需求,babel/preset-react
已经提供了配置方式:
{
test: /\.jsx/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-react",
{
"pragma": "Leact.createElement",
"pragmaFrag": "Leact.Fragment",
"throwIfNamespace": false,
"runtime": "classic"
}
]
]
}
}
},
只需要这样配置就能将 jsx 转换为调用自己编写的 createElement 进行处理。
webpack
由于对 webpack 更熟悉因此采用 webpack 作为项目打包的工具。一般来说,webpack 的配置文件都包含 dev 以及 prod 两份。dev 主要是为了本地测试框架的正确性,其配置文件为:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.bundle.js',
},
devtool: 'source-map',
devServer: {
static: './dist',
},
mode: 'development',
module: {
rules: [
{
test: /\.jsx/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-react",
{
"pragma": "Leact.createElement",
"pragmaFrag": "Leact.Fragment",
"throwIfNamespace": false,
"runtime": "classic"
}
]
]
}
}
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [new HtmlWebpackPlugin({template: './main.html'})],
};
除了 jsx 以外,还需要 ts-loader
将 typescript 转换为 js。对于 prod 环境,只需要打包为一个 js 文件,因此不需要 jsx,配置如下:
const path = require('path');
module.exports = {
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, './lib'),
filename: 'leact.min.js',
libraryTarget: 'umd',
library: 'leact',
},
mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
}
}
由于 ts-loader 本身是采用来的 tsc 对 typescript 进行转译,因此还需要 tsconfig.json
来配置一些 ts 的特性:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es2017",
"jsxFactory": "Leact.createElement",
"allowJs": true,
"moduleResolution": "node"
}
}
jest
作为一个框架,良好的单元测试也是必不可少的。leact 采用 jest 作为单元测试的框架,因此同样需要配置文件 jest.config.js 来配置 jest 的一些功能,
const config = {
verbose: true,
jest: {
"moduleFileExtensions": ["js", "jsx"],
"moduleDirectories": ["src"],
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(gif|ttf|eot|svg)$": "<rootDir>/__mocks__/fileMock.js"
}
},
// 需要转译 jsx,因此同样需要 babel-jest 进行转换
transform: {
"\\.[jt]sx?$": "babel-jest"
}
};
module.exports = config;
// Or async function
module.exports = async () => {
return {
verbose: true,
};
};
jest 根据 babel-jest 来使用 babel 对单元测试中的 jsx 进行转换,因此同样需要 babel.config.js
来对 babel 进行配置(这里不是 webpack 需要的 babel,只是在执行单测的时候需要):
module.exports = {
"presets": [
[
"@babel/preset-react",
{
"pragma": "Leact.createElement", // default pragma is React.createElement (only in classic runtime)
"pragmaFrag": "Leact.Fragment", // default is React.Fragment (only in classic runtime)
"throwIfNamespace": false, // defaults to true
"runtime": "classic" // defaults to classic
}
],
[
'@babel/preset-env',
{ targets: { node: 'current' } }
],
'@babel/preset-typescript',
]
}
这些配置完成后,将单元测试以 xxx.test.js 的格式放入 __test__
文件夹下就可以被 jest 检测到。运行命令:
- jest: 执行所有单元测试
- jest 特定文件: 只执行该文件中的样例
- jest --coverage: 执行单元测试并输出覆盖率
eslint
为了更好的代码质量,需要使用 eslint 对代码进行一些规范,主要是使用了默认的配置,然后按自己的习惯加入了一些 rule, .eslintrc
如下:
{
"root": true,
"rules": {
"semi": ["error", "never"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline"
}],
"quotes": ["error", "single"],
"@typescript-eslint/no-explicit-any": ["off"]
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
上传到 npm
当我们的代码完成之后,自然也希望能够上传到 npm 作为一个包使用。这里主要有以下步骤:
- 注册 npm 账号
- 正确配置 package.json
- npm adduser 登录用户
- npm publish 上传到 npm 仓库
步骤 3 和步骤 4 有一个需要注意的地方,很多时候我们默认采用淘宝源。因此在登录的时候也是会默认登陆到淘宝的仓库,这样会导致权限问题。这时候可以将默认源切回 npm 官方源,或者使用 --registry
来指定官方源。
package.json
在 package.json 中关于上传到 npm 的配置主要有以下几个:
- main: 打包后的入口文件存放路径,要和 webpack 配置的输出文件一致
- typings:ts 接口文件的位置
- author: 作者
- license: 项目采用的协议
- files:哪些文件需要被包含到包中
最后整个配置如下:
{
"name": "@l1n3x/leact",
"version": "1.0.4",
"description": "a tiny react-like js framework",
"main": "./lib/leact.min.js",
"typings": "src/index.d.ts",
"author": "l1n3x",
"license": "MIT",
"files": [
"src",
"lib",
"package.json",
"readme.md"
],
}
总结
本篇文章主要包括了开发这个框架的一些基础准备工作,关于代码的分析会在后面的文章中给出。