chrome 插件基础模板

一直以来,都觉得 chrome 插件挺方便的,可以做出灵活又好用的工具。工作中也接触过一些插件的开发,但是都没有系统的了解下。这次花点时间记录下。

因为刚好发现这个工具《What is CRXJS》,用 vite 做开发工具,支持前端框架有 react、vue、solid 等。这样一来就省了很多事了,chrome 插件也可以使用漂漂亮亮的组件库了。

具体有《官方文档》,这里记录一下快速使用的点。

工程地址:《chrome-extension-plugin-demo

注意:
1. 最新 chrome 浏览器版本已经禁止的 manifest_version 2 版本的使用了,现在开始都是 3 了。
2. 这个 @crxjs/vite-plugin 工具要使用 beta 版本,latest 在 manifest_version 3 会因为资源安全问题加载报错。

介绍

manifest 的字段为介绍:

{
  "manifest_version": 3,
  "name": "Chrome Extension",
  "version": "1.0.0",
  "description": "Chrome Extension React Vite Example",
  "permissions": ["tabs", "notifications"],
  "action": {
    "default_popup": "src/popup/index.html",
    "default_icon": "src/assets/images/icon.png"
  },
  "icons": {
    "16": "src/assets/images/icon-16.png",
    "32": "src/assets/images/icon-32.png",
    "48": "src/assets/images/icon-48.png",
    "128": "src/assets/images/icon-128.png"
  },
  "background": {
    "service_worker": "src/background/index.ts",
    "type": "module"
  },
  "content_scripts": [
    {
      "js": ["src/scripts/content.ts"],
      "matches": ["<all_urls>"]
    }
  ]
}
  • permissions:声明要开通的模块,不然调用 api 会失败。
  • action:就是右上角插件面板里,那个按钮点击后弹出框的入口文件和显示图标。
  • background:后台运行的代码,这里可以调用的 api 更全一点。可以指定以 es module 模式加载。
  • content_scripts:会嵌入到当前访问网站里,可以读取当前网站的 dom,storage 等,实际就是当前网站的一部分了。matches 可以指定哪些网站可以嵌入。具体可以看《匹配模式》。

示例工程,用了多页面,但是这个 @crxjs/vite-plugin 只默认加载了 action 做入口文件。所以要自己配一下其它入口页面。

import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { crx, type ManifestV3Export } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

export default defineConfig({
  plugins: [react(), crx({ manifest: manifest as ManifestV3Export })],
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'src/main/index.html'),
      },
    },
  },
});

开发

像往常一样启动 vite 工程就行,只不过配置了开发时也把文件存在硬盘,因为要丢到插件库里去运行。执行 pnpm dev 后就会产出 dist 文件夹,浏览器打开 chrome://extensions/,然后把整个 dist 文件夹往浏览器界面拖就行。当然右上角的开发者模式要打开。
chrome://extensions 页面 “所有扩展程序” 下,有自己的开发程序那就是成功了。大部分代码更新都是可以马上生效不需要刷新加载的。改了配置之类的就得刷新加载,此时点击该插件面板右下角的刷新按钮就行了。

react、vue、solid 实现低代码 ssr 页面渲染

项目做了个低代码平台,复盘下,本地实现一个丐版。主要是实现渲染部分,没有做设计器,也就是拖拉拽部分。

代码地址:brick-schema-ssr

对于低代码的一些想法

低代码 这个概念已经很久了,可能大家或多或少都做过类似的,很多公司也会去尝试自己内部自研。老实话,我感觉这玩意定制性挺强的,做到能开源或者通用平台真的不容易。放业界两个优秀的产品:lowcode-enginemybricks

个人觉得还是 schema 的设计,决定了设计器应该怎么交互。毕竟设计器最终就是为了产出相应的 schema。而且最复杂的其实是“动起来”。也就是界面拖拉拽拼接好后,怎么加上逻辑编排,让页面动起来。这是个麻烦活,设置简单了,功能不够强大。设置复杂了,上手难,还得了解怎么用。

可以从场景出发,一个是快速上新一些小的单页面活动页,减少重复工作。一个是可以交由设计师来直接制作页面,有点像以前的 Dreamweaver 啊。说白了都是快,为了提效。当然这里的快还包含了上线快,毕竟改了 schema 就可以生效了,不然直接拷贝代码也很快。其他的复杂页面,我认为还不如直接写代码,出问题还不好排查,甚至还不好维护。但如果写一大堆代码,那还叫低代码么。

工程介绍

工程使用了 pnpm + monorepo,打包工具用 vite,前端各自用 react、vue、solid,后端用 koa。

  • packages
    • core
      • app:提供 App.tsx,给各个框架做入门组件使用。
      • components:各个组件的主要实现,包括 props、styles、classes 等处理,不多,就实现了五个做 demo。
      • library:一些需要外部注入的工具,比如一些 hooks,useState 等。
      • sdk:各框架渲染 sdk 的基类,就是实现一些公共方法,剩下的框架相关的各自去实现。
      • types:类型声明,其中的 dom.d.ts 声明,是从 vue3 抄过来的。
    • react:react 渲染 sdk,提供 createRoothydraterenderToString 等功能,继承自 core 里的 sdk 基类,同时实现并注入框架相关的 hook。
    • vue: 同 react,引擎改为 vue。
    • solid:同 react,引擎改为 solid。
    • client:前端项目,react、vue、solid 三个框架结合各自 sdk,接受 schema 做 demo 展示,顺便提供打包后的 index 给后端做返回页面用。
    • server
      • render:使用 react、vue、solid 三个 sdk 提供的 renderToString 方法,加上编译处理,用于生产。
      • src:koa 服务,使用 render 文件夹导出的 renderToString 方法,返回不同框架的 schema ssr 页面。以及其他的接口支持。

对于逻辑编排的实现,我这里直接采用写代码的方式了。

后话

实际上我们还支持了 qwik.js,据说是个 ssr 很厉害的框架。同时还支持 react-native,就是把 html 的 dom 标签,替换成 rn 提供的原生组件。这个适配巨复杂,本来就是个原生应用,只是改成开发像前端。也写了相关几篇文章:《react-native ios 流水账》、《ios 原生应用集成 react-native》、《android 原生应用集成 react-native》。

其实也是为了适配而适配,真正用的就是一套就够了。而且组件库一般也不会从基础搭建起,都是使用现有的组件库,最多改点东西适配一下。

真的有这种适配多框架的需求,可以用这个「Mitosis」,用 jsx 写组件,可以编译成多个前端库的对应组件,包括:Angular, React, Qwik, Vue, Svelte, Solid, and React Native。也就是按照它要求的有一定约束的写法,就是写出一套适配多框架的组件库。

js 几种导入依赖排序 import sort

前言

现在前端工程化一般都会加上 eslint + prettier (从零搭建前端工程(下))做格式化,为了团队风格,美观,还有为了减少不必要的冲突,比如 import 文件的顺序。合并代码的时候,冲突无可避免,但是能尽量减少最好。能够统一排序,添加的文件的改动也比较一目了然。相应的工具其实也不少,大同小异,选择适合自己的而用之。

一、eslint 自带的 sort-imports

sort-imports》。不看了,自定义差,还几乎不能 auto fix。

二、eslint 插件 eslint-plugin-import

eslint-plugin-import》。支持自定义配置,支持 auto fix,后面说的都是,不然没啥意义了。示例代码:

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 'latest',
  },
  plugins: ['import'],
  rules: {
    'import/order': [
      'error',
      {
        groups: [
          'index',
          'builtin',
          'external',
          'internal',
          'object',
          'type',
          'unknown',
          ['parent', 'sibling'],
        ],
        pathGroups: [
          {
            pattern: 'react*',
            group: 'builtin',
            position: 'after',
          },
          {
            pattern: '@/*',
            group: 'internal',
            position: 'after',
          },
          {
            pattern: '@*/**',
            group: 'internal',
            position: 'before',
          },
        ],
        pathGroupsExcludedImportTypes: [],
        distinctGroup: false,
        'newlines-between': 'always',
        alphabetize: {
          order: 'asc',
          orderImportKind: 'asc',
          caseInsensitive: true,
        },
        warnOnUnassignedImports: false,
      },
    ],
  },
};

大部分配置都很好理解,就是这个 pathGroupsExcludedImportTypes 实在不明所以。

Issue#2156 有个老哥给出答案:
想要 pathGroups 的配置生效,那么它原本所属的类型就不要出现在 pathGroupsExcludedImportTypes react* 属于 external,而这个属性的默认值是 [‘buildin’, ‘external’],所以配置了 react* 的话,就要重定义这个值。 所以示例代码是空数组。
老哥又提到:But now, it has been applied exactly the opposite way.

囧,确实我理解反了,我以为是要使其生效才要写在里面。而且理解成是 pattern 定义的分类是属于下面 group 的,也就是一开始以为是指定 react* 属于 builtin。而其实不是,而是说 react* 在 builtin 类型的相对位置。= =!

Read More

依赖反转/注入与 IOC 容器 inversify

一、概念

先看看概念释义(百度)- 依赖反转原则:

在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

我第一个想法就是反转什么?再看看释义(chatgpt):

“反转” 指的是将依赖关系从具体实现转向抽象,这样高层模块就不会直接依赖低层模块的实现细节,而是依赖于抽象接口或抽象类。依赖注入则是实现这一反转原则的一个具体手段,通过将依赖项注入到类中,减少了类与其依赖项之间的耦合。

结合给的例子,虽然是 java 的例子,不过大同小异。

传统实现:

public class UserService {
    private UserRepository userRepository;
    public UserService() {
        this.userRepository = new UserRepository(); // 依赖于具体实现
    }
    public void someServiceMethod() {
        userRepository.someMethod();
    }
}

应用依赖反转原则的实现:

// 定义抽象
public interface UserRepository {
    void someMethod();
}
// 实现具体细节
public class UserRepositoryImpl implements UserRepository {
    public void someMethod() {
        // 实现细节
    }
}
// 高层模块依赖于抽象
public class UserService {
    private UserRepository userRepository;
    // 依赖注入
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    public void someServiceMethod() {
        userRepository.someMethod();
    }
}

那么依赖注入又是什么,如果接触过 angular 的话,应该会知道这个概念。释义(chatgpt):

依赖注入是一种实现依赖反转的方法,通过将依赖项(如 UserRepository)注入到 UserService 中,而不是由 UserService 自行创建依赖项。依赖注入可以通过多种方式实现,例如:

  • 构造器注入:通过构造函数将依赖项注入类。
  • 属性注入:通过类的属性(setter 方法)注入依赖项。
  • 接口注入:通过实现特定接口注入依赖项(这种方式在实践中不太常见)。

个人理解:依赖反转是一种编程思想,而依赖注入是其原则的一种实现方式。反转了对依赖的使用。

  • 传统:直接在类里面声明使用一个依赖。如果需求上要改动依赖,就得改动调用方,或者重写一个新的调用方,去调用新的一个依赖。
  • 依赖反转:给调用方传递一个依赖,大家约定好依赖要实现某个接口(方法),调用方就直接用。如此一来,如果需要更改依赖方法,就传递一个新的依赖进去就好。调用方的代码就不用改。
    比如:跑自动化测试和正式使用,就可以给某些功能注入不同的 service,来达到适配不同环境。

个人觉得叫依赖注入还好理解点。不过一个是指导思想,一个是实施方案嘛。

Read More

android 原生应用集成 react-native

一、前言

继上次尝试了《ios 原生应用集成 react-native》之后。这次尝试 android 原生应用的集成。android 也分两种语言,kotlin 和 java。这里就尝试 java 就好了,不和 ios 一样尝试两种语言。全屏模式用 ReactRootView 组件,窗口模式使用 Fragment 组件做承载。有没有点眼熟,这个在 web 里代表空标签。vue 少见点, react 应该挺常见的。也有专门的 api,document.createDocumentFragment

这里也不多说,主要是记录下官网《ntegration with Existing Apps》教程里没有的。大部分还是跟着官网走就可以。老样子,也是建立在正常的 react-native android 环境能运行的前提下,才能继续。按照官网《Setting up the development environment》说的,JDK,Android Studio 该装的装好。

还是那句话,app 开发的同学应该一看就懂了,我这还是以一个只懂前端的视角记录一下。

二、全屏集成

创建按钮很显眼,就不用多说。这里说下两个模版,第一行第二个 Empty Activity,据说是新模板,开发语言只有 kotlin。第二行第二个 Empty Views Activity,据说是旧模板,开发语言可以选 kotlin 和 java,这里选 java。至于最下面还有个选择 build 语言的,选择第一个 Kotlin DSL(build.gradle.kts) 就可以了,反正三个我都看了都和官网示例对没完全对上。

1. MyReactActivity.java

创建一份 MyReactActivity.java 文件,如果是新项目的话,会有一份 MainActivity.java,放在一起就好了。

// 记得声明自己应用
package com.example.myapplicationrn;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.KeyEvent;

import com.facebook.react.PackageList;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.soloader.SoLoader;

import java.util.List;

public class MyReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {

    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SoLoader.init(this, false);

        mReactRootView = new ReactRootView(getApplication());
        List<ReactPackage> packages = new PackageList(getApplication()).getPackages();
        // 有一些第三方可能不能自动链接,对于这些包我们可以用下面的方式手动添加进来:
        // packages.add(new MyReactNativePackage());
        // 同时需要手动把他们添加到`settings.gradle`和 `app/build.gradle`配置文件中。

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackages(packages)
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 注意这里的MyReactNativeApp 必须对应"index.js"中的
        // "AppRegistry.registerComponent()"的第一个参数
        mReactRootView.startReactApplication(mReactInstanceManager, "reactnativeandroid", null);

        setContentView(mReactRootView);
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    protected void onPause() {
        super.onPause();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostPause(this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostResume(this, this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
        }
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostDestroy(this);
            mReactInstanceManager.destroy();
        }
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }
}

其实就是官网示例,然后整合成一份了。再把一些依赖 import 写上,不然完全新手看一堆依赖缺失还是挺不好的。

Read More