ARouter完全解析(一)源码详解

前言

在前面的文章组件化中用到了ARouter框架,它是专门用来做组件化改造的,官方定义如下:

一个用于帮助Android App进行组件化改造的框架 ------ 支持模块间的路由、通信、解耦

什么是路由?可能你首先联想到的是路由器,路由器根据路由表来转发数据包,路由表决定了数据传输的路径。ARouter就相当于一个路由器,让无依赖的双方可以通信。

下面我们就通过源码来分析ARouter的实现原理,本文ARouter源码基于:com.alibaba:arouter-api:1.5.2。

源码解析

ARouter的简单使用在前面的文章中已有介绍,使用前需要在Application中用ARouter.init(this)来初始化ARouter,下面我们就从这里开始分析源码:

java 复制代码
public final class ARouter {

	 private volatile static boolean hasInit = false;

    public static void init(Application application) {
        //判断是否已经初始化
        if (!hasInit) {
            ...
            //调用_ARouter.init()初始化
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }
            ...
        }
    }
}

首先判断有没有初始化过,已经初始化过后把hasInit置为true,ARouter的init()方法调用了_ARouter的init()方法:

java 复制代码
final class _ARouter {

	 private volatile static boolean hasInit = false;
	
	 private volatile static ThreadPoolExecutor executor = DefaultPoolExecutor.getInstance();
	
    protected static synchronized boolean init(Application application) {
        mContext = application;
        LogisticsCenter.init(mContext, executor);
        ...
        hasInit = true;
        return true;
    }
}

_ARouter的init()方法中出现了LogisticsCenter这个类,这个类是做什么的呢?看类名是 "物流中心"的意思 ,继续看下它的init()方法做了些什么:

java 复制代码
public class LogisticsCenter {

   //加载内存中的路由信息 
	public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        ...
        //从插件中加载路由表
        loadRouterMap();
        if (registerByPlugin) {
            logger.info(TAG, "Load router map by arouter-auto-register plugin.");
        } else {
            Set<String> routerMap;

            // It will rebuild router map every times when debuggable.
            // 调用ARouter.openDebug()、第一次运行、版本更新都会更新routerMap。
            // PackageUtils.isNewVersion()中通过SharedPreference存储的App的
            // versionName和versionCode来判断版本是否有更新
            if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {

                // These class was generated by arouter-compiler.
                // 这里就是从dex中获取com.alibaba.android.arouter.routes包下的.class
                // ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes";
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                if (!routerMap.isEmpty()) {
                    //把routerMap存入SharedPreference
                    context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                }
				
		     //更新SharedPreference中存储的App的版本信息
                PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
            } else {
                //直接从SP中拿routerMap,就是前面保存在SP中的routerMap
                routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
            }
		 ...
            // 根据className,来实例化不同的对象并调用loadInto()方法。
            for (String className : routerMap) {
                //以com.alibaba.android.arouter.routes.ARouter$$Root开头
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } 
                //以com.alibaba.android.arouter.routes.ARouter$$Interceptors开头
                else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } 
                //以com.alibaba.android.arouter.routes.ARouter$$Providers开头
                else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }
        }
    }

	/**
     * arouter-auto-register plugin will generate code inside this method
     * call this method to register all Routers, Interceptors and Providers
     */
    private static void loadRouterMap() {
        registerByPlugin = false;
        // auto generate register code by gradle plugin: arouter-auto-register
        // looks like below:
        // registerRouteRoot(new ARouter..Root..modulejava());
        // registerRouteRoot(new ARouter..Root..modulekotlin());
    }
}

从上面的代码可以看出,这段代码就是加载路由表的核心代码,上面有注释标出了一些代码的业务逻辑,这里再挑出几个比较难理解的地方重点讲解一下,首先是这句代码loadRouterMap(),注释上说的是从插件中加载路由表,什么意思呢?就是如果我们想缩短ARouter初始化的时间,可以用ARouter的Gradle插件,这个插件能自动加载路由表,这样ARouter初始化的时候就不需要读取类的信息,从而缩短初始化时间。

第22行从dex中获取所有com.alibaba.android.arouter.routes包下的.class文件放入routerMap,从第36行开始遍历routerMap,分成了3种情况:

  1. 如果.class文件以com.alibaba.android.arouter.routes.ARouter$$Root开头则反射创建该实例,并调用该实例的loadInto()方法,loadInto()方法传入的参数为Warehouse.groupsIndex;
  2. 如果.class文件以com.alibaba.android.arouter.routes.ARouter$$Interceptors开头则反射创建该实例,并调用该实例的loadInto()方法,loadInto()方法传入的参数为Warehouse.interceptorsIndex;
  3. 如果.class文件以com.alibaba.android.arouter.routes.ARouter$$Providers开头则反射创建该实例,并调用该实例的loadInto()方法,loadInto()方法传入的参数为Warehouse.providersIndex;

在前面的文章中,选择集成调试模式,Sync Project后,在download模块下的build\intermediates\javac\debug\classes\com\alibaba\android\arouter\routes目录下共生成了3个.class文件,这些文件是由注解处理器生成的:

ruby 复制代码
ARouter$$Group$$download.class
ARouter$$Providers$$download.class
ARouter$$Root$$download.class

其中ARouter$$Root$$download.class以com.alibaba.android.arouter.routes.ARouter$$Root开头,点开ARouter$$Root$$download.class,其代码如下:

java 复制代码
public class ARouter$$Root$$download implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("download", ARouter$$Group$$download.class);
  }
}

其loadInto()方法将ARouter$$Group$$download.class存入routes中,routes为Map<String, Class<? extends IRouteGroup>>类型,key为download,routes就是上面的Warehouse.groupsIndex。Warehouse是存储路由信息的仓库,其代码如下:

java 复制代码
class Warehouse {
    //用于缓存实现了IRouteGroup接口的类,key为path的第一级
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    //用于缓存路由元信息,key为path
    static Map<String, RouteMeta> routes = new HashMap<>();
    
    //缓存实现了IProvider接口的类的实例,key为IProvider实现类的class
    static Map<Class, IProvider> providers = new HashMap<>();
    //缓存实现了IProvider类的路由元信息,key为继承了IProvider的接口的路径
    static Map<String, RouteMeta> providersIndex = new HashMap<>();

    //缓存interceptor
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();

    static void clear() {
        routes.clear();
        groupsIndex.clear();
        providers.clear();
        providersIndex.clear();
        interceptors.clear();
        interceptorsIndex.clear();
    }
}

这样就把ARouter$$Group$$download.class存入了Warehouse.groupsIndex这个HashMap中,key为download。

接下来分析跳转的代码,跳转代码如下:

java 复制代码
ARouter.getInstance().build(path).navigation();

跟进去:

java 复制代码
public final class ARouter {

    private volatile static ARouter instance = null;

    private ARouter() {
    }

    public static ARouter getInstance() {
        if (!hasInit) {
            throw new InitException("ARouter::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (ARouter.class) {
                    if (instance == null) {
                        instance = new ARouter();
                    }
                }
            }
            return instance;
        }
    }
    
    public Postcard build(String path) {
        return _ARouter.getInstance().build(path);
    }
    
}

在getInstance()方法中先判断hasInit是否为true,如果为false抛出异常,接下来用单例模式创建ARouter实例,build()方法调用了_ARouter.getInstance().build(path):

java 复制代码
final class _ARouter {

    private _ARouter() {
    }

    protected static _ARouter getInstance() {
        if (!hasInit) {
            throw new InitException("ARouterCore::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (_ARouter.class) {
                    if (instance == null) {
                        instance = new _ARouter();
                    }
                }
            }
            return instance;
        }
    }
    
    /**
     * Build postcard by path and default group
     */
    protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                //替换path
                path = pService.forString(path);
            }
            return build(path, extractGroup(path), true);
        }
    }
    
    //从传入的path中解析出group
    private String extractGroup(String path) {
        ...
        String defaultGroup = path.substring(1, path.indexOf("/", 1));
        if (TextUtils.isEmpty(defaultGroup)) {
            throw new HandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!");
        } else {
            return defaultGroup;
        }
        ...
    }
    
    /**
     * Build postcard by path and group
     * 通过path和group拿到Postcard
     */
    protected Postcard build(String path, String group, Boolean afterReplace){
        ...
        return new Postcard(path, group);
    }

}

第25行先判断,如果path为空抛出异常,如果想替换path,可以写一个类实现PathReplaceService接口。接着调用第33行的build()方法,这里从path中解析出group,最后新建Postcard实例,传入path和group。Postcard从名字翻译过来是明信片的意思,其承载了一次路由需要的所有信息,Postcard代码如下:

java 复制代码
/**
 * A container that contains the roadmap.
 */
public final class Postcard extends RouteMeta {

    private Uri uri;
    private Bundle mBundle;         // Data to transform

    public Postcard() {
        this(null, null);
    }

    public Postcard(String path, String group) {
        this(path, group, null, null);
    }

    public Postcard(String path, String group, Uri uri, Bundle bundle) {
        setPath(path);
        setGroup(group);
        setUri(uri);
        this.mBundle = (null == bundle ? new Bundle() : bundle);
    }
   

    /**
     * Navigation to the route with path in postcard.
     * No param, will be use application context.
     */
    public Object navigation() {
        return navigation(null);
    }

    /**
     * Navigation to the route with path in postcard.
     *
     * @param context Activity and so on.
     */
    public Object navigation(Context context) {
        return navigation(context, null);
    }

    /**
     * Navigation to the route with path in postcard.
     *
     * @param context Activity and so on.
     */
    public Object navigation(Context context, NavigationCallback callback) {
        return ARouter.getInstance().navigation(context, this, -1, callback);
    }
}

Postcard继承自路由元信息RouteMeta,传入path和group实际调用的是RouteMeta的setPath()方法和setGroup()方法,对RouteMeta中的path和group成员变量赋值:

java 复制代码
/**
 * It contains basic route information.
 */
public class RouteMeta {
    private RouteType type;         // Type of route
    private Element rawType;        // Raw type of route
    private Class<?> destination;   // Destination
    private String path;            // Path of route
    private String group;           // Group of route
    
    public String getPath() {
        return path;
    }

    public RouteMeta setPath(String path) {
        this.path = path;
        return this;
    }

    public String getGroup() {
        return group;
    }

    public RouteMeta setGroup(String group) {
        this.group = group;
        return this;
    }
}

接下来看看navigation()方法做了什么?

java 复制代码
public final class Postcard extends RouteMeta {

    public Object navigation() {
        return navigation(null);
    }

    /**
     * Navigation to the route with path in postcard.
     *
     * @param context Activity and so on.
     */
    public Object navigation(Context context) {
        return navigation(context, null);
    }

    /**
     * Navigation to the route with path in postcard.
     *
     * @param context Activity and so on.
     */
    public Object navigation(Context context, NavigationCallback callback) {
        return ARouter.getInstance().navigation(context, this, -1, callback);
    }
}    

调用了ARouter的navigation()方法并传入Postcard实例:

java 复制代码
public final class ARouter {

    /**
     * Launch the navigation.
     */
    public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
        return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
    }
}

这里又调用了_ARouter的navigation()方法:

java 复制代码
final class _ARouter {
    /**
     * Use router navigation.
     */
    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        //若有PretreatmentService的实现,就进行预处理,可以在真正路由前进行一些判断然后中断路由。
        PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
        if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {
            // Pretreatment failed, navigation canceled.
            return null;
        }

        // Set context to postcard.
        postcard.setContext(null == context ? mContext : context);

        try {
            //标记1,完善postcard信息
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            //没有找到路由的回调
            if (null != callback) {
                callback.onLost(postcard);
            } else {
                // No callback for this invoke, then we use the global degrade service.
                DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
                if (null != degradeService) {
                    degradeService.onLost(context, postcard);
                }
            }

            return null;
        }

        //找到路由的回调
        if (null != callback) {
            callback.onFound(postcard);
        }

        //不是绿色通道,要走拦截器
        if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                 //拦截器处理结果:继续路由
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                 //拦截器处理结果:中断路由,回调中断
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            //标记2,绿色通道,不走拦截器,获取路由结果
            return _navigation(postcard, requestCode, callback);
        }

        return null;
    }
}

我们先看看标记1处代码做了什么:

java 复制代码
public class LogisticsCenter {

    /**
     * Completion the postcard by route metas
     *
     * @param postcard Incomplete postcard, should complete by this method.
     */
    public synchronized static void completion(Postcard postcard) {
        //通过path去Warehouse.routes中拿RouteMeta
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        //如果RouteMeta为空
        if (null == routeMeta) {
            // Maybe its does't exist, or didn't load.
            //如果Warehouse.groupsIndex中没有这个group,抛出异常
            if (!Warehouse.groupsIndex.containsKey(postcard.getGroup())) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                // Load route and cache it into memory, then delete from metas.
                //添加RouteMeta到Warehouse.routes
                addRouteGroupDynamic(postcard.getGroup(), null);
                
                //重新执行completion()方法
                completion(postcard);   // Reload
            }
        } else {
            //把routeMeta里面的配置传递给postcard
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            Uri rawUri = postcard.getUri();
            if (null != rawUri) {   // Try to set params into bundle.
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();

                if (MapUtils.isNotEmpty(paramsType)) {
                    // Set value by its type, just for params which annotation by @Param
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                params.getValue(),
                                params.getKey(),
                                resultMap.get(params.getKey()));
                    }

                    // Save params name which need auto inject.
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }

                // Save raw uri
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }

            switch (routeMeta.getType()) {
                //如果类型是PROVIDER
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must implement IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    //去Warehouse.providers中获取实例
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    //如果instance为null
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            //反射创建实例
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            //存入Warehouse.providers
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            logger.error(TAG, "Init provider failed!", e);
                            throw new HandlerException("Init provider failed!");
                        }
                    }
                    //传递给postcard
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
        }
    }
    
    public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        if (Warehouse.groupsIndex.containsKey(groupName)){
            // If this group is included, but it has not been loaded
            // load this group first, because dynamic route has high priority.
            //调用实例的loadInto()方法添加到Warehouse.routes  
            Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
            //从Warehouse.groupsIndex中移除key为groupName的数据
            Warehouse.groupsIndex.remove(groupName);
        }

        // cover old group.
        if (null != group) {
            group.loadInto(Warehouse.routes);
        }
    }
}    

上面的completion()方法先去Warehouse.routes中拿RouteMeta,Warehouse就是前面存储路由信息的仓库,Warehouse.routes是其中缓存路由元信息的HashMap。判断如果RouteMeta为null,调用addRouteGroupDynamic()方法,去Warehouse.groupsIndex中取出缓存的类,使用反射创建该类的实例,然后调用其loadInto()方法,这样就调用了ARouter$$Group$$download.class的loadInto()方法,其代码如下:

java 复制代码
public class ARouter$$Group$$download implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/download/DownloadActivity", RouteMeta.build(RouteType.ACTIVITY, DownloadActivity.class, "/download/downloadactivity", "download", null, -1, -2147483648));
    atlas.put("/download/service", RouteMeta.build(RouteType.PROVIDER, DownloadServiceImpl.class, "/download/service", "download", null, -1, -2147483648));
  }
}

其loadInto()方法把RouteMeta通过build()方法创建的实例添加到Warehouse.routes中。

回到前面的代码,接着又调用了一次completion()方法,此时routeMeta不再为null,将routeMeta的配置信息(destination、type等信息)传递给postcard,如果routeMeta类型是PROVIDER,会反射创建实例并存入Warehouse.providers。最后执行标记2处的代码开始准备跳转,标记2处代码如下:

java 复制代码
final class _ARouter {

    private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = postcard.getContext();

        switch (postcard.getType()) {
            case ACTIVITY:
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (0 != flags) {
                    intent.setFlags(flags);
                }

                // Non activity, need FLAG_ACTIVITY_NEW_TASK
                if (!(currentContext instanceof Activity)) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Set Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });

                break;
            case PROVIDER:
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
                //Broadcast、ContentProvider、Fragment,都是使用postcard.getDestination()反射创建实例
                Class<?> fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }
}

上面的代码没什么好说的,先判断postcard的类型,如果是Activity类型,就把postcard里面的参数传递给Intent,最终还是调用startActivity()方法进行跳转;如果是Provider类型,直接通过postcard.getProvider()拿到IProvier的实现类的单例;Broadcast、ContentProvider、Fragment都是使用postcard.getDestination()反射创建实例并返回。

参考资料: juejin.cn/post/720004...

相关推荐
Developer_Niuge11 分钟前
前端批量请求失败重复弹窗的正确解决方案
前端
前端小饭桌12 分钟前
告别嵌套地狱:用数据结构优化解决 JS 多层循环的混乱与静默错误
前端·javascript
爱摸鱼的格子13 分钟前
🚀 你真的会用 Promise.all 吗?10 个实用技巧助你成为异步处理大师!
前端
JacksonGao14 分钟前
React Fiber的调度算法你了解多少呢?
前端·react.js
这可不简单16 分钟前
方便易懂的自适应方案---echarts和dom样式大小自适应
前端·vue.js·echarts
玲小珑18 分钟前
Auto.js 入门指南(七)定时任务调度
android·前端
橘黄的猫18 分钟前
深入解析 import.meta.url:与 new URL() 的关系及 Vite 中的 base 路径影响
前端·vite
白瓷梅子汤20 分钟前
跟着官方示例学习 @tanStack-table --- Column Filters
前端·react.js
海的诗篇_23 分钟前
前端开发面试题总结-HTML篇
前端·面试·html
Nano23 分钟前
JavaScript ES6:现代Web开发的革命性进化
前端·javascript