在本文中,我们将为您详细介绍ReactServer端CSS模块渲染的相关知识,并且为您解答关于reactcssmodule的疑问,此外,我们还会提供一些关于ApacheSeaTunnelZeta引擎源
在本文中,我们将为您详细介绍React Server端CSS模块渲染的相关知识,并且为您解答关于react css module的疑问,此外,我们还会提供一些关于Apache SeaTunnel Zeta 引擎源码解析(一)Server端的初始化、create-react-app创建react项目 css模块化处理、create-react-app创建出来项目,如何设置CSS模块化?、Egg + React (React Router + Redux) (SSR) 服务端渲染实际的有用信息。
本文目录一览:- React Server端CSS模块渲染(react css module)
- Apache SeaTunnel Zeta 引擎源码解析(一)Server端的初始化
- create-react-app创建react项目 css模块化处理
- create-react-app创建出来项目,如何设置CSS模块化?
- Egg + React (React Router + Redux) (SSR) 服务端渲染实际
React Server端CSS模块渲染(react css module)
CSS与React组件的当前做法似乎是使用webpack的样式加载器将其加载到页面中。
import React, { Component } from ''react'';import style from ''./style.css'';class MyComponent extends Component { render(){ return ( <div className={style.demo}>Hello world!</div> ); }}
通过这样做,样式加载器会将<style>
元素注入DOM。但是,<style>
不会出现在虚拟DOM中,因此如果进行服务器端渲染,<style>
则将省略。这导致页面具有FOUC。
还有其他方法可以加载在服务器和客户端均可使用的CSS模块吗?
答案1
小编典典您可以看一下isomorphic-style-loader。加载程序是专门为解决此类问题而创建的。
但是,使用此_insertCss()
方法必须使用加载程序提供的方法。该文档详细说明了如何使用它。
希望能有所帮助。
Apache SeaTunnel Zeta 引擎源码解析(一)Server端的初始化
引入
本系列文章是基于 Apache SeaTunnel 2.3.6版本,围绕Zeta
引擎给大家介绍其任务是如何从提交到运行的全流程,希望通过这篇文档,对刚刚上手SeaTunnel的朋友提供一些帮助。
我们整体的文章将会分成三篇,从以下方向给大家介绍:
- SeaTunnel Server端的初始化
- Client端的任务提交流程
- Server端的接收到任务的执行流程
由于涉及源码解析,涉及篇幅较大,所以分成系列文章来记录下一个任务的整体流程。
参考
- [ST-Engine The Design of LogicalPlan to PhysicalPlan](https://github.com/apache/seatunnel/issues/2269):https://github.com/apache/seatunnel/issues/2269
作者介绍
大家好,我是刘乃杰,一名大数据开发工程师,参与Apache SeaTunnel的开发也有一年多的时间了,不仅给SeaTunnel提交了一些PR,而且添加的一些功能也非常有意思,欢迎大家来找我交流,其中包括支持Avro格式文件,SQL Transform中支持嵌套结构查询,给节点添加Tag达到资源隔离等。
近期推送SeaTunnel在公司内部的落地,需要跟同事,老板介绍SeaTunnel的技术架构,也需要详细的运行流程,帮助同事更好的上手开发,维护。
但是发现目前好像没有这样的一篇文章,能从整体来分析一个任务的执行流程,从而帮助开发者更加容易的定位问题,添加功能。
所以花了一些时间来写了这篇文章,希望抛砖引玉让其他的大佬们也多多写一些源码解析的文章出来。
集群拓扑
首先请大家先从整体了解下SeaTunnel的Zeta引擎架构, SeaTunnel是基于hazelcast来实现的分布式集群通信。
在2.3.6版本之后, 集群中的节点可以被分配为Master或Worker节点,从而将调度与执行分开,避免Master节点的负载过高从而出现问题。
并且2.3.6版本还添加了一个功能,可以对每个节点添加tag
属性,当提交任务时可以通过tag
来选择任务将要运行的节点, 从而达到资源隔离的目的。
集群的服务端分为Master或Worker节点, Master节点负责接收请求、逻辑计划生成、分配任务等(与之前的版本相比,会多了几个Backup节点,但是对于集群稳定性来说是一个挺大的提升)。
而Worker节点则只负责执行任务, 也就是数据的读取和写入。
提交任务时可以创建Hazelcast的客户端连接集群来进行通信,或者使用Restapi来进行通信。
服务端启动
当我们对集群的整体架构有个大致的了解后,再来具体了解下具体的流程。
首先看下Server端的启动过程。
Server端的启动命令为:
sh bin/seatunnel-cluster.sh -d -r <node role type>
查看这个脚本的内容后就会发现, 这个脚本最终的执行命令为:
java -cp seatunnel-starter.jar org.apache.seatunnel.core.starter.seatunnel.SeaTunnelServer <other_java_jvm_config_and_args>
我们查看这个starter.seatunnel.SeaTunnelServer
的代码
public class SeaTunnelServer { public static void main(String[] args) throws CommandException { ServerCommandArgs serverCommandArgs = CommandLineUtils.parse( args, new ServerCommandArgs(), EngineType.SEATUNNEL.getStarterShellName(), true); SeaTunnel.run(serverCommandArgs.buildCommand()); } }
这个代码是使用了JCommander
来解析用户传递的参数并构建并运行Command
, serverCommandArgs.buildCommand
返回的类为:
public class ServerExecuteCommand implements Command<ServerCommandArgs> { private final ServerCommandArgs serverCommandArgs; public ServerExecuteCommand(ServerCommandArgs serverCommandArgs) { this.serverCommandArgs = serverCommandArgs; } @Override public void execute() { SeaTunnelConfig seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig(); String clusterRole = this.serverCommandArgs.getClusterRole(); if (StringUtils.isNotBlank(clusterRole)) { if (EngineConfig.ClusterRole.MASTER.toString().equalsIgnoreCase(clusterRole)) { seaTunnelConfig.getEngineConfig().setClusterRole(EngineConfig.ClusterRole.MASTER); } else if (EngineConfig.ClusterRole.WORKER.toString().equalsIgnoreCase(clusterRole)) { seaTunnelConfig.getEngineConfig().setClusterRole(EngineConfig.ClusterRole.WORKER); // in hazelcast lite node will not store IMap data. seaTunnelConfig.getHazelcastConfig().setLiteMember(true); } else { throw new SeaTunnelEngineException("Not supported cluster role: " + clusterRole); } } else { seaTunnelConfig .getEngineConfig() .setClusterRole(EngineConfig.ClusterRole.MASTER_AND_WORKER); } HazelcastInstanceFactory.newHazelcastInstance( seaTunnelConfig.getHazelcastConfig(), Thread.currentThread().getName(), new SeaTunnelNodeContext(seaTunnelConfig)); } }
在这里会根据配置的角色类型来修改配置信息。
当是Worker节点时,将Hazelcast节点的类型设置为lite member
,在Hazelcast中lite member是不进行数据存储的
然后会创建了一个hazelcast
实例, 并且传递了SeaTunnelNodeContext
实例以及读取并修改的配置信息
public class SeaTunnelNodeContext extends DefaultNodeContext { private final SeaTunnelConfig seaTunnelConfig; public SeaTunnelNodeContext(@NonNull SeaTunnelConfig seaTunnelConfig) { this.seaTunnelConfig = seaTunnelConfig; } @Override public NodeExtension createNodeExtension(@NonNull Node node) { return new org.apache.seatunnel.engine.server.NodeExtension(node, seaTunnelConfig); } @Override public Joiner createJoiner(Node node) { JoinConfig join = getActiveMemberNetworkConfig(seaTunnelConfig.getHazelcastConfig()).getJoin(); join.verify(); if (node.shouldUseMulticastJoiner(join) && node.multicastService != null) { super.createJoiner(node); } else if (join.getTcpIpConfig().isEnabled()) { log.info("Using LiteNodeDropOutTcpIpJoiner TCP/IP discovery"); return new LiteNodeDropOutTcpIpJoiner(node); } else if (node.getProperties().getBoolean(DISCOVERY_SPI_ENABLED) || isAnyAliasedConfigEnabled(join) || join.isAutoDetectionEnabled()) { super.createJoiner(node); } return null; } private static boolean isAnyAliasedConfigEnabled(JoinConfig join) { return !AliasedDiscoveryConfigUtils.createDiscoveryStrategyConfigs(join).isEmpty(); } private boolean usePublicAddress(JoinConfig join, Node node) { return node.getProperties().getBoolean(DISCOVERY_SPI_PUBLIC_IP_ENABLED) || allUsePublicAddress( AliasedDiscoveryConfigUtils.aliasedDiscoveryConfigsFrom(join)); } }
在SeaTunnelNodeContext
中覆盖了createNodeExtension
方法, 将使用engine.server.NodeExtension
类,
这个类的代码为:
public class NodeExtension extends DefaultNodeExtension { private final NodeExtensionCommon extCommon; public NodeExtension(@NonNull Node node, @NonNull SeaTunnelConfig seaTunnelConfig) { super(node); extCommon = new NodeExtensionCommon(node, new SeaTunnelServer(seaTunnelConfig)); } @Override public void beforeStart() { // TODO Get Config from Node here super.beforeStart(); } @Override public void afterStart() { super.afterStart(); extCommon.afterStart(); } @Override public void beforeClusterStateChange( ClusterState currState, ClusterState requestedState, boolean isTransient) { super.beforeClusterStateChange(currState, requestedState, isTransient); extCommon.beforeClusterStateChange(requestedState); } @Override public void onClusterStateChange(ClusterState newState, boolean isTransient) { super.onClusterStateChange(newState, isTransient); extCommon.onClusterStateChange(newState); } @Override public Map<String, Object> createExtensionServices() { return extCommon.createExtensionServices(); } @Override public TextCommandService createTextCommandService() { return new TextCommandServiceImpl(node) { { register(HTTP_GET, new Log4j2HttpGetCommandProcessor(this)); register(HTTP_POST, new Log4j2HttpPostCommandProcessor(this)); register(HTTP_GET, new RestHttpGetCommandProcessor(this)); register(HTTP_POST, new RestHttpPostCommandProcessor(this)); } }; } @Override public void printNodeInfo() { extCommon.printNodeInfo(systemLogger); } }
在这个代码中, 我们可以看到在构造方法中, 初始化了SeaTunnelServer
这个类, 而这个类与最开始的类是同名的,
是在不同的包下, 这个类的完整类名为: org.apache.seatunnel.engine.server.SeaTunnelServer
我们看下这个类的代码:
public class SeaTunnelServer implements ManagedService, MembershipAwareService, LiveOperationsTracker { private static final ILogger LOGGER = Logger.getLogger(SeaTunnelServer.class); public static final String SERVICE_NAME = "st:impl:seaTunnelServer"; private NodeEngineImpl nodeEngine; private final LiveOperationRegistry liveOperationRegistry; private volatile SlotService slotService; private TaskExecutionService taskExecutionService; private ClassLoaderService classLoaderService; private CoordinatorService coordinatorService; private ScheduledExecutorService monitorService; @Getter private SeaTunnelHealthMonitor seaTunnelHealthMonitor; private final SeaTunnelConfig seaTunnelConfig; private volatile boolean isRunning = true; public SeaTunnelServer(@NonNull SeaTunnelConfig seaTunnelConfig) { this.liveOperationRegistry = new LiveOperationRegistry(); this.seaTunnelConfig = seaTunnelConfig; LOGGER.info("SeaTunnel server start..."); } @Override public void init(NodeEngine engine, Properties hzProperties) { ... if (EngineConfig.ClusterRole.MASTER_AND_WORKER.ordinal() == seaTunnelConfig.getEngineConfig().getClusterRole().ordinal()) { startWorker(); startMaster(); } else if (EngineConfig.ClusterRole.WORKER.ordinal() == seaTunnelConfig.getEngineConfig().getClusterRole().ordinal()) { startWorker(); } else { startMaster(); } ... } .... }
这个类是SeaTunnel Server端的核心代码, 在这个类中会根据节点的角色来启动相关的组件。
稍微总结下seatunnel的流程:
SeaTunnel是借助于Hazelcast的基础能力,来实现集群端的组网, 并调用启动核心的代码。
对于这一块有想深入了解的朋友可以去看下Hazelcast的相关内容,这里仅仅列出了调用路径。
按照顺序所加载调用的类为
- starter.SeaTunnelServer
- ServerExecutreCommand
- SeaTunnelNodeContext
- NodeExtension
- server.SeaTunnelServer
接下来再来详细看下Master节点以及Worker节点中所创建的组件
Worker节点
private void startWorker() { taskExecutionService = new TaskExecutionService( classLoaderService, nodeEngine, nodeEngine.getProperties()); nodeEngine.getMetricsRegistry().registerDynamicMetricsProvider(taskExecutionService); taskExecutionService.start(); getSlotService(); } public SlotService getSlotService() { if (slotService == null) { synchronized (this) { if (slotService == null) { SlotService service = new DefaultSlotService( nodeEngine, taskExecutionService, seaTunnelConfig.getEngineConfig().getSlotServiceConfig()); service.init(); slotService = service; } } } return slotService; }
我们可以看到在startWorker
方法中, 会初始化两个组件, taskExectutionService
和slotService
这两个组件, 都与任务执行相关。
SlotService
先来看下SlotService
的初始化
@Override public void init() { initStatus = true; slotServiceSequence = UUID.randomUUID().toString(); contexts = new ConcurrentHashMap<>(); assignedSlots = new ConcurrentHashMap<>(); unassignedSlots = new ConcurrentHashMap<>(); unassignedResource = new AtomicReference<>(new ResourceProfile()); assignedResource = new AtomicReference<>(new ResourceProfile()); scheduledExecutorService = Executors.newSingleThreadScheduledExecutor( r -> new Thread( r, String.format( "hz.%s.seaTunnel.slotService.thread", nodeEngine.getHazelcastInstance().getName()))); if (!config.isDynamicSlot()) { initFixedSlots(); } unassignedResource.set(getNodeResource()); scheduledExecutorService.scheduleAtFixedRate( () -> { try { LOGGER.fine( "start send heartbeat to resource manager, this address: " + nodeEngine.getClusterService().getThisAddress()); sendToMaster(new WorkerHeartbeatOperation(getWorkerProfile())).join(); } catch (Exception e) { LOGGER.warning( "failed send heartbeat to resource manager, will retry later. this address: " + nodeEngine.getClusterService().getThisAddress()); } }, 0, DEFAULT_HEARTBEAT_TIMEOUT, TimeUnit.MILLISECONDS); }
在SeaTunnel中会有一个动态Slot的概念,如果设置为true
, 则每个节点就没有Slot的这样一个概念,可以提交任意数量的任务到此节点上,如果设置为固定数量的Slot, 那么该节点仅能接受这些Slot数量的Task运行。
在初始化时,会根据是否为动态Slot来进行数量的初始化。
private void initFixedSlots() { long maxMemory = Runtime.getRuntime().maxMemory(); for (int i = 0; i < config.getSlotNum(); i++) { unassignedSlots.put( i, new SlotProfile( nodeEngine.getThisAddress(), i, new ResourceProfile( CPU.of(0), Memory.of(maxMemory / config.getSlotNum())), slotServiceSequence)); } }
同时也可以看到初始化时会启动一个线程,定时向Master节点发送心跳,心跳信息中则包含了当前节点的信息, 包括已经分配的、未分配的Slot数量等属性,Worker节点通过心跳将信息定时更新给Master。
@Override public synchronized WorkerProfile getWorkerProfile() { WorkerProfile workerProfile = new WorkerProfile(nodeEngine.getThisAddress()); workerProfile.setProfile(getNodeResource()); workerProfile.setAssignedSlots(assignedSlots.values().toArray(new SlotProfile[0])); workerProfile.setUnassignedSlots(unassignedSlots.values().toArray(new SlotProfile[0])); workerProfile.setUnassignedResource(unassignedResource.get()); workerProfile.setAttributes(nodeEngine.getLocalMember().getAttributes()); workerProfile.setDynamicSlot(config.isDynamicSlot()); return workerProfile; } private ResourceProfile getNodeResource() { return new ResourceProfile(CPU.of(0), Memory.of(Runtime.getRuntime().maxMemory())); }
TaskExecutionService
这个组件与任务提交相关, 这里先简单看下,与任务提交的相关代码在后续再深入查看。
在Worker节点初始化时, 会新建一个TaskExecutionService
对象,并调用其start
方法
private final ExecutorService executorService = newCachedThreadPool(new BlockingTaskThreadFactory()); public TaskExecutionService( ClassLoaderService classLoaderService, NodeEngineImpl nodeEngine, HazelcastProperties properties) { // 加载配置信息 seaTunnelConfig = ConfigProvider.locateAndGetSeaTunnelConfig(); this.hzInstanceName = nodeEngine.getHazelcastInstance().getName(); this.nodeEngine = nodeEngine; this.classLoaderService = classLoaderService; this.logger = nodeEngine.getLoggingService().getLogger(TaskExecutionService.class); // 指标相关 MetricsRegistry registry = nodeEngine.getMetricsRegistry(); MetricDescriptor descriptor = registry.newMetricDescriptor() .withTag(MetricTags.SERVICE, this.getClass().getSimpleName()); registry.registerStaticMetrics(descriptor, this); // 定时任务更新指标到IMAP中 scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); scheduledExecutorService.scheduleAtFixedRate( this::updateMetricsContextInImap, 0, seaTunnelConfig.getEngineConfig().getJobMetricsBackupInterval(), TimeUnit.SECONDS); serverConnectorPackageClient = new ServerConnectorPackageClient(nodeEngine, seaTunnelConfig); eventBuffer = new ArrayBlockingQueue<>(2048); // 事件转发服务 eventForwardService = Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setNameFormat("event-forwarder-%d").build()); eventForwardService.submit( () -> { List<Event> events = new ArrayList<>(); RetryUtils.RetryMaterial retryMaterial = new RetryUtils.RetryMaterial(2, true, e -> true); while (!Thread.currentThread().isInterrupted()) { try { events.clear(); Event first = eventBuffer.take(); events.add(first); eventBuffer.drainTo(events, 500); JobEventReportOperation operation = new JobEventReportOperation(events); RetryUtils.retryWithException( () -> NodeEngineUtil.sendOperationToMasterNode( nodeEngine, operation) .join(), retryMaterial); logger.fine("Event forward success, events " + events.size()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.info("Event forward thread interrupted"); } catch (Throwable t) { logger.warning( "Event forward failed, discard events " + events.size(), t); } } }); } public void start() { runBusWorkSupplier.runNewBusWork(false); }
在这个类中,有一个成员变量,创建了一个线程池。
在构造方法中创建了一个定时任务来更新IMAP里面的任务状态。创建了一个任务来将Event信息发送给Master节点,由Master节点将这些Event发送给外部服务。
Master节点
private void startMaster() { coordinatorService = new CoordinatorService(nodeEngine, this, seaTunnelConfig.getEngineConfig()); monitorService = Executors.newSingleThreadScheduledExecutor(); monitorService.scheduleAtFixedRate( this::printExecutionInfo, 0, seaTunnelConfig.getEngineConfig().getPrintExecutionInfoInterval(), TimeUnit.SECONDS); }
我们可以看到在Master节点中,启动了两个组件:协调器组件和监控组件。
监控组件的任务比较简单, 就是周期性的打印集群信息。
CoordinatorService
public CoordinatorService( @NonNull NodeEngineImpl nodeEngine, @NonNull SeaTunnelServer seaTunnelServer, EngineConfig engineConfig) { this.nodeEngine = nodeEngine; this.logger = nodeEngine.getLogger(getClass()); this.executorService = Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("seatunnel-coordinator-service-%d") .build()); this.seaTunnelServer = seaTunnelServer; this.engineConfig = engineConfig; masterActiveListener = Executors.newSingleThreadScheduledExecutor(); masterActiveListener.scheduleAtFixedRate( this::checkNewActiveMaster, 0, 100, TimeUnit.MILLISECONDS); } private void checkNewActiveMaster() { try { if (!isActive && this.seaTunnelServer.isMasterNode()) { logger.info( "This node become a new active master node, begin init coordinator service"); if (this.executorService.isShutdown()) { this.executorService = Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("seatunnel-coordinator-service-%d") .build()); } initCoordinatorService(); isActive = true; } else if (isActive && !this.seaTunnelServer.isMasterNode()) { isActive = false; logger.info( "This node become leave active master node, begin clear coordinator service"); clearCoordinatorService(); } } catch (Exception e) { isActive = false; logger.severe(ExceptionUtils.getMessage(e)); throw new SeaTunnelEngineException("check new active master error, stop loop", e); } }
在初始化时, 会启动一个线程来定时检查当前阶段是否为Master节点, 当节点当前不是Master节点但在集群中成为Master节点时, 会调用initCoordinatorService()
来进行状态的初始化, 并将状态修改为True。
当节点自身标记为Master节点,但在集群中已不再是Master节点时,进行状态清理。
private void initCoordinatorService() { // 从hazelcast中获取分布式IMAP runningJobInfoIMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_RUNNING_JOB_INFO); runningJobStateIMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_RUNNING_JOB_STATE); runningJobStateTimestampsIMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_STATE_TIMESTAMPS); ownedSlotProfilesIMap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_OWNED_SLOT_PROFILES); metricsImap = nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_RUNNING_JOB_METRICS); // 初始化JobHistoryService jobHistoryService = new JobHistoryService( runningJobStateIMap, logger, runningJobMasterMap, nodeEngine.getHazelcastInstance().getMap(Constant.IMAP_FINISHED_JOB_STATE), nodeEngine .getHazelcastInstance() .getMap(Constant.IMAP_FINISHED_JOB_METRICS), nodeEngine .getHazelcastInstance() .getMap(Constant.IMAP_FINISHED_JOB_VERTEX_INFO), engineConfig.getHistoryJobExpireMinutes()); // 初始化EventProcess, 用于发送事件到其他服务 eventProcessor = createJobEventProcessor( engineConfig.getEventReportHttpApi(), engineConfig.getEventReportHttpHeaders(), nodeEngine); // If the user has configured the connector package service, create it on the master node. ConnectorJarStorageConfig connectorJarStorageConfig = engineConfig.getConnectorJarStorageConfig(); if (connectorJarStorageConfig.getEnable()) { connectorPackageService = new ConnectorPackageService(seaTunnelServer); } // 集群恢复后, 尝试恢复之前的历史任务 restoreAllJobFromMasterNodeSwitchFuture = new PassiveCompletableFuture( CompletableFuture.runAsync( this::restoreAllRunningJobFromMasterNodeSwitch, executorService)); }
在Coordinatorservice
中,会拉取分布式MAP,这个数据结构是Hazelcast的一个数据结构,可以认为是在集群中数据一致的一个MAP。
在SeaTunnel中, 使用这个结构来存储任务信息、Slot信息等。
在这里还会创建EventProcessor
, 这个类是用来将事件通知到其他服务,比如任务失败,可以发送信息到配置的接口中,实现事件推送。
最后,由于节点启动,可能是集群异常重启,或者节点切换,这时需要恢复历史运行的任务,那么就会从刚刚获取到的IMAP中获取到之前正在跑的任务列表,然后尝试进行恢复。
这里的IMAP信息可以开启持久化将信息存储到HDFS等文件系统中, 这样可以在系统完全重启后仍然能够读取到之前的任务状态并进行恢复。
在CoordinatorService
中运行的组件有:
- executorService (所有可能被选举为master的节点)
- jobHistoryService (master节点)
- eventProcessor (master节点)
Master节点与备选节点上会:
- 定时检查自己是否为Master节点, 如果是则进行相应的状态转化
Master节点上会:
- 定时打印集群的状态信息。
- 启动转发服务, 将要推送的事件转发到外边服务
在Worker节点上, 启动后会:
- 定时将状态信息上报到Master节点
- 将任务信息更新到IMAP里面。
- 将在Worker产生的要推送给外部服务的事件转发到Master节点上。
至此, 服务端所有服务组件都已启动完成,本文完!
本文由 白鲸开源科技 提供发布支持!
create-react-app创建react项目 css模块化处理
用的css预处理器用sass,其他大同小异。
用create-react-app创建项目,执行npm run eject弹出配置文件(此操作不可逆);
配置sass,用的最新的CRA,webpack4;
webpack.config.dev.js (webpack.config.prod.js需相同配置一份):
{
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/, /\.sass$/, /\.scss$/],
loader: require.resolve(''file-loader''),
options: {
name: ''static/media/[name].[hash:8].[ext]'',
},
},
{
test: /\.(scss|sass)$/,
use: [
''style-loader'',
{ loader: ''css-loader'', options: { importLoaders: 1,modules: true } },
''sass-loader''
]
// loaders: [''style-loader'', ''css-loader'', ''sass-loader'']
}
其中modules: true 意思是开启模块化css处理
css:
.body
border: 1px solid red
.body-wrapper
border: 2px solid red
页面引入:

有些样式不需要编译,就要:global(className)来标识这个类是全局类名,因此CSS Modules不对其类名进行转化:
结构:
import React, { Component } from "react";
import indexCss from ''../../css/style.scss'';
export default class Index extends Component{
render() {
return(
<div className={indexCss["body-wrapper"]}>
<div className={indexCss.body}>
<span>关于我们</span>
<span className={indexCss.name}>name</span>
</div>
<div className="title">title标题</div>
</div>
);
}
}
样式:
.body{
border: 1px solid red;
}
.body-wrapper{
padding: 50px;
border: 2px solid red;
.name{
color: red;
}
}
:global(.title) {
color: blue
}
效果:
create-react-app创建出来项目,如何设置CSS模块化?
问题起因
最近在研究react,根据官方提供的create-react-app脚手架搭建了一个项目,但是像下面这样引入CSS的时候,会发现全局都会生效,这明显不合理嘛,那么,怎么设置呢?
import ''./header.css''; // 这样引入相当于全局引入了
CSS模块化
我们知道,在webpack中,可以通过CSS-loader和style-loader对CSS文件进行一些打包处理,里面有一些配置项,可以将CSS打包成一个模块,然后引入,一般webpack是这样设置的
{
test: /\.css$/,
loader: ''style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]_[local]__[hash:base64:5]''
}
其中modules为true时,就是把CSS打包为模块的控制参数
如何设置?
用create-react-app创建出来的项目,我们得先找到那个配置文件在哪,文件默认路径为:\node_modules\react-scripts\config\webpack.config.dev.js,然后找到如下代码,新增一个参数modules:true就可以了:
{
test: /\.css$/,
use: [
require.resolve(''style-loader''),
{
loader: require.resolve(''css-loader''),
options: {
importLoaders: 1,
modules: true
},
},
{
loader: require.resolve(''postcss-loader''),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: ''postcss'',
plugins: () => [
require(''postcss-flexbugs-fixes''),
autoprefixer({
browsers: [
''>1%'',
''last 4 versions'',
''Firefox ESR'',
''not ie < 9'', // React doesn''t support IE8 anyway
],
flexbox: ''no-2009'',
}),
],
},
},
],
},
最后
在你需要用到的模块引入
import headerCss from ''./header.css'';
当然,别忘了生产线的配置也改下,也就是webpack.config.prod.js文件
Egg + React (React Router + Redux) (SSR) 服务端渲染实际
概述
在实现 Egg + React 服务端渲染解决方案 egg-react-webpack-boilerplate 时,因在 React + React Router + Redux 方面没有深入的实践过以及精力问题, 只实现了多页面服务端渲染方案。最近收到社区的一些咨询,想知道 Egg + React Router + Redux 如何实现 SPA 同构实现。如是就开始了 Egg + React Router + Redux 的摸索之路,实践过程中遇到 React-Router 版本问题,Redux 使用问题等问题,折腾了几天,但最终还是把想要的方案实践出来。
摸索阶段
在查阅 react router 和 redux 的相关资料,发现 react router 有 V3 和 V4 版本, V4 新版本又分为 react-router,react-router-dom,react-router-config,react-router-redux 插件, redux 相关的有 redux,react-redux,只能硬着头皮一个一个看看啥含义,看一下简单的Todo例子, 相比 Vue 的 vuex + vue-router 的工程搭建过程,这个要复杂的多,只好采用分阶段完成。先完成了纯前端渲染的 React Router + Redux 结合的例子,把 React Router 和 Redux 的相关 API 撸了一遍,基本掌握 React-Redux actions, reducer, store使用(这里自己先通过简单的例子让整个流程跑通,然后逐渐添砖加瓦,实现自己想要的功能. 比如不考虑异步,不考虑数据请求,直接hack数据,跑通后,再逐渐改造完善)。
依赖说明
react router(v4)
react-router |
React Router 核心 |
---|---|
react-router-dom |
用于 DOM 绑定的 React Router |
react-router-native |
用于 React Native 的 React Router |
react-router-redux |
React Router 和 Redux 的集成 |
react-router-config |
静态路由配置辅助 |
// 客户端用BrowserRouter, 服务端渲染用 StaticRouter 静态路由组件 import { BrowserRouter, StaticRouter } from ''react-router-dom'';
redux 和 react-redux
这里直接借个图(https://segmentfault.com/a/1190000011473973):
Redux 介绍
Redux 是 javaScript 状态管理容器
通过 Redux 可以很方便进行数据集中管理和实现组件之间的通信,同时视图和数据逻辑分离,对于大型复杂(业务复杂,交互复杂,数据交互频繁等)的 React 项目, Redux 能够让代码结构(数据查询状态、数据改变状态、数据传播状态)层次更合理。另外,Redux 和 React 之间没有关系。Redux 支持 React、Angular、jQuery 甚至纯 JavaScript。
Redux 的设计思想很简单
Redux是在借鉴Flux思想上产生的,基本思想是保证数据的单向流动,同时便于控制、使用、测试
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面,也就是单一数据源
Redux 核心由三部分组成:Store, Action, Reducer。
- Store : 贯穿你整个应用的数据都应该存储在这里。
// component/spa/ssr/actions 创建store,初始化store数据 export function create(initalState){ return createStore(reducers, initalState); }
- Action: 必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。除此之外的属性,就是进行这个操作需要的数据。
// component/spa/ssr/actions export function add(item) { return { type: ADD, item } } export function del(id) { return { type: DEL, id } }
- Reducer: 是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的state。
// component/spa/ssr/reducers export default function update(state, action) { const newState = Object.assign({}, state); if (action.type === ADD) { const list = Array.isArray(action.item) ? action.item : [action.item]; newState.list = [...newState.list, ...list]; } else if (action.type === DEL) { newState.list = newState.list.filter(item => { return item.id !== action.id; }); } else if (action.type === LIST) { newState.list = action.list; } return newState }
redux 使用
// store的创建 var createStore = require(''redux'').createStore; var store = createStore(update); // store 里面的数据发生改变时,触发的回调函数 store.subscribe(function () { console.log(''the state:'', store.getState()); }); // action触发state改变的唯一方法, 改变store里面的方法 store.dispatch(add({id:1, title:''redux''})); store.dispatch(del(1));
react-redux
react-redux 对 redux 流程的一种简化,可以简化手动 dispatch 繁琐过程。 react-redux 重要提供以下两个API,详细介绍请见:http://cn.redux.js.org/docs/react-redux/api.html
- connect(mapStateToProps, mapDispatchToProps, mergeToProps)(App)
- provider
更多信息请参考 http://cn.redux.js.org/
服务端渲染同构实现
页面模板实现
- home.jsx
// component/spa/ssr/components/home.jsx import React, { Component } from ''react'' import { connect } from ''react-redux'' import { add, del } from ''component/spa/ssr/actions''; class Home extends Component { // 服务端渲染调用,这里mock数据,实际请改为服务端数据请求 static fetch() { return Promise.resolve({ list:[{ id: 0, title: `Egg+React 服务端渲染骨架`, summary: ''基于Egg + React + Webpack3/Webpack2 服务端渲染同构工程骨架项目'', hits: 550, url: ''https://github.com/hubcarl/egg-react-webpack-boilerplate'' }, { id: 1, title: ''前端工程化解决方案easywebpack'', summary: ''programming instead of configuration, webpack is so easy'', hits: 550, url: ''https://github.com/hubcarl/easywebpack'' }, { id: 2, title: ''前端工程化解决方案脚手架easywebpack-cli'', summary: ''easywebpack command tool, support init Vue/Reac/Weex boilerplate'', hits: 278, url: ''https://github.com/hubcarl/easywebpack-cli'' }] }).then(data => { return data; }) } render() { const { add, del, list } = this.props; const id = list.length + 1; const item = { id, title: `Egg+React 服务端渲染骨架-${id}`, summary: ''基于Egg + React + Webpack3/Webpack2 服务端渲染骨架项目'', hits: 550 + id, url: ''https://github.com/hubcarl/egg-react-webpack-boilerplate'' }; return <div className="redux-nav-item"> <h3>SPA Server Side</h3> <div className="container"> <div className="row row-offcanvas row-offcanvas-right"> <div className="col-xs-12 col-sm-9"> <ul className="smart-artiles" id="articleList"> {list.map(function(item) { return <li key={item.id}> <div className="point">+{item.hits}</div> <div className="card"> <h2><a href={item.url} target="_blank">{item.title}</a></h2> <div> <ul className="actions"> <li> <time className="timeago">{item.moduleName}</time> </li> <li className="tauthor"> <a href="#" target="_blank" className="get">Sky</a> </li> <li><a>+收藏</a></li> <li> <span className="timeago">{item.summary}</span> </li> <li> <span className="redux-btn-del" onClick={() => del(item.id)}>Delete</span> </li> </ul> </div> </div> </li>; })} </ul> </div> </div> </div> <div className="redux-btn-add" onClick={() => add(item)}>Add</div> </div>; } } function mapStateToProps(state) { return { list: state.list } } export default connect(mapStateToProps, { add, del })(Home)
- about.jsx
// component/spa/ssr/components/about.jsx import React, { Component } from ''react'' export default class About extends Component { render() { return <h3 className="spa-title">React+Redux+React Router SPA Server Side Render Example</h3>; } }
react-router 路由定义
// component/spa/ssr/ssr import { connect } from ''react-redux'' import { BrowserRouter, Route, Link, Switch } from ''react-router-dom'' import Home from ''component/spa/ssr/components/home''; import About from ''component/spa/ssr/components/about''; import { Menu, Icon } from ''antd''; const tabKey = { ''/spa/ssr'': ''home'', ''/spa/ssr/about'': ''about'' }; class App extends Component { constructor(props) { super(props); const { url } = props; this.state = { current: tabKey[url] }; } handleClick(e) { console.log(''click '', e, this.state); this.setState({ current: e.key, }); }; render() { return <div> <Menu onClick={this.handleClick.bind(this)} selectedKeys={[this.state.current]} mode="horizontal"> <Menu.Item key="home"> <Link to="/spa/ssr">SPA-Redux-Server-Side-Render</Link> </Menu.Item> <Menu.Item key="about"> <Link to="/spa/ssr/about">About</Link> </Menu.Item> </Menu> <Switch> <Route path="/spa/ssr/about" component={About}/> <Route path="/spa/ssr" component={Home}/> </Switch> </div>; } } export default App;
SPA前端渲染同构实现
import React, { Component } from ''react'' import ReactDOM from ''react-dom'' import { Provider } from ''react-redux'' import {match, RouterContext} from ''react-router'' import { BrowserRouter, StaticRouter } from ''react-router-dom''; import { matchRoutes, renderRoutes } from ''react-router-config'' import Header from ''component/layout/standard/header/header''; import SSR from ''component/spa/ssr/ssr''; import { create } from ''component/spa/ssr/store''; import routes from ''component/spa/ssr/routes'' const store = create(window.__INITIAL_STATE__); const url = store.getState().url; ReactDOM.render( <div> <Header></Header> <Provider store={ store }> <BrowserRouter> <SSR url={ url }/> </BrowserRouter> </Provider> </div>, document.getElementById(''app'') );
SPA服务端渲染同构实现
在服务端渲染时,这里纠结了一下,遇到两个问题
- 参考一些资料的写法Node服务端都是在路由里面处理的,写起来好别扭, 希望 render时
- ReactDOMServer.renderToString(ReactElement) 参数必须是ReactElement
- 组件异步获取的数据Node render怎么获取到
这里通过函数回调的方式可以解决上面问题,也就是 export 出去的是一个函数,然后 render 判断是否直接renderToString还是调用函数,然后再进行renderToString。目前在 egg-view-react-ssr 做了一层简单判断,代码如下:
app.react.renderElement = (reactElement, locals, options) => { if (reactElement.prototype && reactElement.prototype.isReactComponent) { return Promise.resolve(app.react.renderToString(reactElement, locals)); } const context = { state: locals }; return reactElement(context, options).then(element => { return app.react.renderToString(element, context.state); }); }
这样处理了以后,Node 服务端controller处理时就无需自己处理路由匹配问题和store问题,全部交给底层处理。现在的这种处理方式与Vue服务端渲染render思路一致,把服务端逻辑写到模板文件里面,然后由Webpack构建js文件。
SPA服务端渲染入口文件
Webpack 构建的文件 app/ssr.js
到 app/view 目录
import React, { Component } from ''react'' import ReactDOM from ''react-dom'' import { Provider } from ''react-redux'' import {match, RouterContext} from ''react-router'' import { BrowserRouter, StaticRouter } from ''react-router-dom''; import { matchRoutes, renderRoutes } from ''react-router-config'' import Header from ''component/layout/standard/header/header''; import SSR from ''component/spa/ssr/ssr''; import { create } from ''component/spa/ssr/store''; import routes from ''component/spa/ssr/routes'' // context 为服务端初始化数据 export default function(context, options) { const url = context.state.url; // 根据服务端url地址找到匹配的组件 const branch = matchRoutes(routes, url); // 收集组件数据 const promises = branch.map(({route}) => { const fetch = route.component.fetch; return fetch instanceof Function ? fetch() : Promise.resolve(null) }); // 获取组件数据,然后初始化store, 同时返回ReactElement return Promise.all(promises).then(data => { const initState = {}; data.forEach(item => { Object.assign(initState, item); }); context.state = Object.assign({}, context.state, initState); const store = create(initState); return () =>( <div> <Header></Header> <Provider store={store}> <StaticRouter location={url} context={{}}> <SSR url={url}/> </StaticRouter> </Provider> </div> ) }); };
Node服务端controller调用
- controller 实现
exports.ssr = function* (ctx) { yield ctx.render(''spa/ssr.js'', { url: ctx.url }); };
- 路由配置
app.get(''/spa(/.+)?'', app.controller.spa.spa.ssr);
- 效果演示
服务端实现与普通模板渲染调用无差异,写起来简单明了。如果你对 Egg + React 技术敢兴趣,赶快来玩一玩 egg-react-webpack-boilerplate 项目吧!
我们今天的关于React Server端CSS模块渲染和react css module的分享就到这里,谢谢您的阅读,如果想了解更多关于Apache SeaTunnel Zeta 引擎源码解析(一)Server端的初始化、create-react-app创建react项目 css模块化处理、create-react-app创建出来项目,如何设置CSS模块化?、Egg + React (React Router + Redux) (SSR) 服务端渲染实际的相关信息,可以在本站进行搜索。
本文标签: