我造了一个迷你 react - 起步篇

218 阅读4分钟

我造了一个迷你 react - 起步篇

动机

关于 react 的源码也研究了很久,也输出了很多关于 react 的文章。由于学过的东西不用很快就会忘,因此决定根据自己对 react 的理解造一个类 react 的轮子。不过既然是迷你版,那么当然只支持小部分 react 的特性:

  1. 为了减轻复杂度,仅支持同步渲染,不支持时间切片
  2. 仅支持函数式组件
  3. 支持部分 hooks(useState, useRef, useEffect, useLayoutEffect)

开发这个迷你版轮子,一方面是为了更好的理解 react,将学到的知识应用到实践中。一方面也是顺便了解一下 end-to-end 的组件开发配置/开发过程。由于是迷你版的 react,所以我将其命名为 Leact(LiteReact)

目前整个项目已经完成预计的功能,代码地址:

各位大佬有兴趣的可以点个 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 作为一个包使用。这里主要有以下步骤:

  1. 注册 npm 账号
  2. 正确配置 package.json
  3. npm adduser 登录用户
  4. 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"
  ],
}

总结

本篇文章主要包括了开发这个框架的一些基础准备工作,关于代码的分析会在后面的文章中给出。