首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 源码学习之 MyBatis 的底层查询原理

导读 本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文可以详细了解MyBatis的一次查询过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析MyBatis一次查询的流程,让大家了解MyBatis的查询原理。 1 问题现象 1.1 场景问题复现 如下图所示,在示例Mapper中,下面提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentName的List集合 List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} </foreach> </if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} </if> </where> </select> 期望运行的结果是 select * from student WHERE student_name IN ( 'lct' , 'lct2' ) 但是实际上运行的结果是 ==> Preparing: select * from student WHERE student_name IN ( ? , ? ) AND student_name = ? ==> Parameters: lct(String), lct2(String), lct2(String) <== Columns: id, student_name, age <== Row: 2, lct2, 2 <== Total: 1 通过运行结果可以看到,没有给student_name单独赋值,但是经过MyBatis解析以后,单独给student_name赋值了一个值,可以推断出MyBatis在解析SQL并对变量赋值的时候是有问题的,初步猜测是foreach循环中的变量的值带到了foreach外边,导致SQL解析出现异常,下面通过源码进行分析验证 2 MyBatis查询原理 2.1 MyBatis架构 2.1.1 架构图 先简单来看看MyBatis整体上的架构模型,从整体上看MyBatis主要分为四大模块: 接口层:主要作用就是和数据库打交道 数据处理层:数据处理层可以说是MyBatis的核心,它要完成两个功能: 通过传入参数构建动态SQL语句; SQL语句的执行以及封装查询结果集成List<E> 框架支撑层:主要有事务管理、连接池管理、缓存机制和SQL语句的配置方式 引导层:引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式 2.1.2 MyBatis四大对象 贯穿MyBatis整个框架的有四大核心对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯穿了整个框架的执行过程,四大对象的主要作用为: ParameterHandler:设置预编译参数 ResultSetHandler:处理SQL的返回结果集 StatementHandler:处理sql语句预编译,设置参数等相关工作 Executor:MyBatis的执行器,用于执行增删改查操作 2.2 从源码解读MyBatis的一次查询过程 首先给出复现问题的代码以及相应的准备过程 2.2.1 数据准备 CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_name` varchar(255) NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES (1, 'lct', 1); INSERT INTO `student` VALUES (2, 'lct2', 2); 2.2.2 代码准备 1.mapper配置文件 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="mybatis.StudentDao"> <!-- 映射关系 --> <resultMap id="resultMap" type="mybatis.Student"> <id column="id" property="id" jdbcType="BIGINT" /> <result column="student_name" property="studentName" jdbcType="VARCHAR" /> <result column="age" property="age" jdbcType="INTEGER" /> </resultMap> <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} </foreach> </if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} </if> </where> </select> </mapper> 2.示例代码 public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取接口的代理类对象 StudentDao mapper = sqlSession.getMapper(StudentDao.class); StudentCondition condition = new StudentCondition(); List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); //执行方法 List<Student> students = mapper.queryStudents(condition); } 2.2.3 查询过程分析 1.SqlSessionFactory的构建 先看SqlSessionFactory的对象的创建过程 //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法 public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } 调用自身的build方法 图1 build方法自身调用调试图例 在这个方法里会创建一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,然后调用parse方法进行解析 图2 parse解析入参调试图例 在这个方法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties> <property name="dialect" value="MYSQL" /> <!-- SQL方言 --> </properties> 然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置 图3 解析配置调试图例 以获取mappers节点的配置来看具体的解析过程 <mappers> <mapper resource="mappers/StudentMapper.xml"/> </mappers> 进入mapperElement方法 mapperElement(root.evalNode("mappers")); 图4 mapperElement方法调试图例 看到MyBatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中 public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } 通过调用configurationElement方法来解析配置的每一个mapper文件 private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } } 以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的 进入buildStatementFromContext方法 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } 可以看到MyBatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置 图5 parseStatementNode方法调试图例 解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 可以看到一个mappedstatement中包含了一个增删改查标签的详细信息 图7 mappedstatement对象方法调试图例 而一个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements 图8 config对象方法调试图例 具体的流程 图9 SqlSessionFactory对象的构建过程 图9 SqlSessionFactory对象的构建过程 2.SqlSession的创建过程 SqlSessionFactory创建完成以后,接下来看看SqlSession的创建过程 SqlSession sqlSession = sqlSessionFactory.openSession(); 首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方法 @Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } 在这个方法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 事务创建完成以后开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } 创建executor以后,会执行executor = (Executor) interceptorChain.pluginAll(executor)方法,这个方法对应的含义是使用每一个拦截器包装并返回executor,最后调用DefaultSqlSession方法创建SqlSession 图10 SqlSession对象的创建过程 3.Mapper的获取过程 有了SqlSessionFactory和SqlSession以后,就需要获取对应的Mapper,并执行mapper中的方法 StudentDao mapper = sqlSession.getMapper(StudentDao.class); 在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因此通过调用 org.apache.ibatis.binding.MapperRegistry#getMapper方法来获取对应的mapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } 在MyBatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类以后执行newInstance方法,获取到对应的实例,这样就可以通过这个实例进行方法的调用 public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } } 获取mapper的流程为 图11 Mapper的获取过程 4.查询过程 获取到mapper以后,就可以调用具体的方法 //执行方法 List<Student> students = mapper.queryStudents(condition); 首先会调用 org.apache.ibatis.binding.MapperProxy#invoke的方法,在这个方法中,会调用org.apache.ibatis.binding.MapperMethod#execute public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } 首先根据SQL的类型增删改查决定执行哪个方法,在此执行的是SELECT方法,在SELECT中根据方法的返回值类型决定执行哪个方法,可以看到在select中没有selectone单独方法,都是通过selectList方法,通过调用 org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法来获取到数据 @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 在selectList中,首先从configuration对象中获取MappedStatement,在statement中包含了Mapper的相关信息,然后调用 org.apache.ibatis.executor.CachingExecutor#query()方法 图12 query()方法调试图示 在这个方法中,首先对SQL进行解析根据入参和原始SQL,对SQL进行拼接 图13 SQL拼接过程代码图示 调用MapperedStatement里的getBoundSql最终解析出来的SQL为 图14 SQL拼接过程结果图示 接下来调用 org.apache.ibatis.parsing.GenericTokenParser#parse对解析出来的SQL进行解析 图15 SQL解析过程图示 最终解析的结果为 图16 SQL解析结果图示 最后会调用SimpleExecutor中的doQuery方法,在这个方法中,会获取StatementHandler,然后调用 org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个方法进行参数和SQL的处理,最后调用statement的execute方法获取到结果集,然后 利用resultHandler对结进行处理 图17 SQL处理结果图示 查询的主要流程为 图18 查询流程处理图示 5.查询流程总结 总结整个查询流程如下 图19 查询流程抽象 2.3 场景问题原因及解决方案 2.3.1 个人排查 这个问bug出现的地方在于绑定SQL参数的时候再源码中位置为 @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } 由于所写的SQL是一个动态绑定参数的SQL,因此最终会走到 org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个方法中去 public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); } // check for nested result maps in parameter mappings (issue #30) for (ParameterMapping pm : boundSql.getParameterMappings()) { String rmId = pm.getResultMapId(); if (rmId != null) { ResultMap rm = configuration.getResultMap(rmId); if (rm != null) { hasNestedResultMaps |= rm.hasNestedResultMaps(); } } } return boundSql; } 在这个方法中,会调用 rootSqlNode.apply(context)方法,由于这个标签是一个foreach标签,因此这个apply方法会调用到 org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个方法中去 @Override public boolean apply(DynamicContext context) { Map<String, Object> bindings = context.getBindings(); final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) { return true; } boolean first = true; applyOpen(context); int i = 0; for (Object o : iterable) { DynamicContext oldContext = context; if (first) { context = new PrefixedContext(context, ""); } else if (separator != null) { context = new PrefixedContext(context, separator); } else { context = new PrefixedContext(context, ""); } int uniqueNumber = context.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { applyIndex(context, i, uniqueNumber); applyItem(context, o, uniqueNumber); } contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { first = !((PrefixedContext) context).isPrefixApplied(); } context = oldContext; i++; } applyClose(context); return true; } 当调用appItm方法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区 private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } 进行绑定参数的时候,绑定完成foreach的方法的时候,可以看到bindings中不止绑定了foreach中的两个参数还额外有一个参数名字studentName->lct2,也就是说最后一个参数也是会出现在bindings这个参数中的, private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } 图20 参数绑定过程 最后判定 org.apache.ibatis.scripting.xmltags.IfSqlNode#apply @Override public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } 可以看到在调用evaluateBoolean方法的时候会把context.getBindings()就是前边提到的bindings参数传入进去,因为现在这个参数中有一个studentName,因此在使用Ognl表达式的时候,判定为这个if标签是有值的因此将这个标签进行了解析 图21 单个参数绑定过程 最终绑定的结果为 图22 全部参数绑定过程 因此这个地方绑定参数的地方是有问题的,至此找出了问题的所在。 2.3.2 官方解释 翻阅MyBatis官方文档进行求证,发现在3.4.5版本发行中bug fixes中有这样一句 图23 此问题官方修复github记录 图23 此问题官方修复github记录 修复了foreach版本中对于全局变量context的修改的bug issue地址为https://github.com/mybatis/mybatis-3/pull/966 修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a 可以看到官方给出的修改方案,重新定义了一个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的问题。 图24 此问题官方修复代码示例 2.3.3 修复方案 升级MyBatis版本至3.4.5以上 如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致 3 源码阅读过程总结 MyBatis源代码的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源码的话,可能还是有一定的难度,没法理解它的运行过程,本次通过一个简单的查询流程从头到尾跟下来,可以看到MyBatis的设计以及处理流程,例如其中用到的设计模式: 图25 MyBatis代码结构图 组合模式:如ChooseSqlNode,IfSqlNode等 模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory 代理模式:MyBatis实现的核心,比如MapperProxy、ConnectionLogger 4 文档参考 https://mybatis.org/mybatis-3/zh/index.htm

优秀的个人博客,低调大师

深入学习 CSS 中的伪元素 ::before 和 ::after

CSS 伪元素用于为元素的指定部分设置样式,作为回顾,先来看下 Mozilla 开发者网站上的解释: 伪元素是一个附加至选择器末的关键词,允许你对被选择元素的特定部分修改样式。例如 ::first-line 伪元素可用于更改段落首行文字的样式。 可用的 CSS 伪元素不是很多,但是,作为前端工程师熟悉它们很重要,每一个都有特定的用途,它们可以极大地改进项目 UI 。 以下是常用的 CSS 伪元素: ::after ::before ::first-letter ::first-line ::marker ::selection 它们看起来都相当简单且不言自明。但是,它们将帮助您构建一些简洁而强大的布局。你觉得哪一款最酷? ::before 和 ::after 是迄今为止最常用的伪元素,它们能够在保持 HTML 和 CSS 最小化的同时做非常酷的事情。 在本文,通过实例回顾一下 ::before 和 ::after 常见的用途。 语法 在开始之前,先来了解一下伪元素的一般语法: selector::pseudo-element { property: value; } 请注意上面的语法使用的是双冒号 (::) 而不是单个冒号 (:),这是 CSS3 语法。最初,CSS 的语法规范只有一个冒号(CSS2、CSS1)。 这一变化是由 W3C 推动的,主要目的是明确区分使用单个冒号的伪类。 它们之间有什么区别?简单来说,伪类是针对非呈现特征进行选择的;另一方面,伪元素是能够创建新的虚拟元素。为了向后兼容,CSS2 和 CSS1 中的单冒号语法在大多数浏览器中仍然被支持。 ::before 和 ::after 如何工作? 这些伪元素用于在目标元素之前或之后添加内容,对内容的放置位置有一个常见的误解。许多人认为内容将放置在所选 HTML 标记之前或之后。相反,它将被放置在元素内容的前面或后面。 让看下面这个例子: p::before { content: "DevPoint - "; } <p>天行无忌</p> 上面的 CSS 和 HTML 实现的效果等价于下面的 HTML: <p>DevPoint - 天行无忌</p> 而不是: DevPoint - <p>天行无忌</p> 就像::before和::after 在元素的内容之前添加内容一样。但有一个不能正常工作的标:<img /> ,作用于这个标签的时候不会在其中添加任何内容。它只是媒体内容的替代品,意味着它不能处理任何伪元素。 content 属性 对于伪元素,content 属性是最主要的,默认情况下,内容设置为无。意味着如果尝试在不添加该属性的情况下设置元素的样式,实际上什么也不会发生,如下代码: p::before { display:block; width: 100%; height: 50px; background-color: #ff0000; } <p>天行无忌</p> 上面的实例代码,对于CSS样式来说没有任何实际意义。 接着在样式表中增加 content 属性,如下: p::before { display:block; width: 100%; height: 50px; content: ""; background-color: #ff0000; } 上面的代码就可以在段落内容前面看到红色的区块。 即使不想添加任何文本,content 属性仍然需要使用 "" ,这是使用伪元素时最容易犯的错误之一。 content 属性功能强大,可以添加各种类型的内容,使用这个属性可以在伪元素上显示任何东西,可以是文本、图片等,在使用的时候需要注意下面两点: content 默认显示为 display-inline ,而不是 display:block; content 为字符串时,用户将无法选择,意味着用户将无法使用鼠标选择复制它。 content 值 可以直接使用类似 content:"DevPoint" 的文本值,如果有过多的内容需要写入到 content 中,直接写在样式表的方式不够灵活。这里介绍一种更加灵活的方式,通过变量传递,如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>CSS 伪元素</title> <meta charset="utf-8" /> <style> p { padding: 1.25rem; font-weight: bold; font-size: 0.75rem; } p::before { content: attr(data-before-text); background-color: #00ffff; margin-right: 0.625rem; padding: 0.625rem; } </style> </head> <body> <p data-before-text="DevPoint">Hello World!</p> </body> </html> 效果如下: 上面的代码,通过元素属性 data-before-text 来为 content 属性传递其值。同样 content 还可以是图片资源,如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>CSS 伪元素</title> <meta charset="utf-8" /> <style> p { padding: 1.25rem; font-weight: bold; font-size: 0.75rem; } p::before { content: url(https://www.devpoint.cn/Apps/Site/View/devpoint/public/images/favicon.png); background-color: #00ffff; margin-right: 0.625rem; padding: 0.625rem; } </style> </head> <body> <p>Hello World!</p> </body> </html> 效果如下: 除了url、属性和字符串之外,content 属性还可以包含多种属性,如 counters 、 quotes 、linear-gradient 等,更多内容可以查阅 MDN Web Docs 上完整的列表。 实例展示 现在来看看 ::before 、 ::after 和 content 结合的常见实例效果。 1. 修饰 主要实例之一主要用于修饰,可以使用它们为元素添加一些视觉效果,如下: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>CSS 伪元素</title> <meta charset="utf-8" /> <style> h1 { font-weight: bold; text-align: center; } h1::after { content: ""; display: block; background-color: #19b5fe; height: 0.2em; width: 100%; } h1::before { content: ""; display: block; background-color: #19b5fe; height: 0.2em; width: 100%; } </style> </head> <body> <h1>DevPoint</h1> </body> </html> 效果如下: 2. blockquote <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>CSS 伪元素</title> <meta charset="utf-8" /> <style> blockquote { font-weight: bold; font-size: 1rem; font-style: italic; } blockquote::before { content: open-quote; font-style: normal; color: #585563; } blockquote::after { content: close-quote " —— " attr(data-author); font-style: normal; color: #585563; } </style> </head> <body> <blockquote data-author="卡耐基"> 机遇固然重要,但不能坐等机遇,不然机遇会白白地从身边溜走。 </blockquote> </body> </html> 3. 排序列表 创建自定义 HTML 有序列表,可以通过使用 counter-reset、counter-increment 和 counter(counter-name)来实现。 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>CSS 伪元素</title> <meta charset="utf-8" /> <style> ol { list-style: none; counter-reset: chapter_counter; } li { margin-bottom: 2.5em; margin-left: 2.5em; position: relative; line-height: 2em; } li::before { content: counter(chapter_counter); counter-increment: chapter_counter; position: absolute; left: -2.8em; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-style: normal; font-size: 0.8em; width: 2em; height: 2em; color: white; background-color: #0566e6; border: 2px solid #0046a1; } </style> </head> <body> <ol> <li>机遇固然重要,但不能坐等机遇,不然机遇会白白地从身边溜走。</li> <li>除了你自己,没有别的能够带给你平静。</li> <li> 一个积极进取的人,总是朝着更高奋斗目标前进,来实现自己的远大理想。 </li> </ol> </body> </html> 总结 本文介绍了::before 和 ::after 两个伪元素基本知识和常见的用法。但并非本文介绍的用法,存在很多意想不到的效果,它们有助于减少不必要的标记并通过丰富元素来增加价值。

优秀的个人博客,低调大师

Angular 服务器端渲染的学习笔记(一)

2 --> 官网链接:https://angular.io/guide/universal Angular Universal, a technology that renders Angular applications on the server. Angular Universal 是一种将 Angular 应用渲染于服务器平台上的技术。 A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. 普通的 Angular 应用在浏览器里执行,响应用户动作,并以 DOM 的方式渲染页面。 Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client. Angular Universal 执行在服务器端,生成静态的应用页面,该页面随后在客户端进行引导(bootstrap). This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive. 服务器端渲染通常意味着应用程序的渲染速度更加快捷,允许用户在应用能够实现正常互动之前,就有机会一窥应用的布局。 google 有一篇专门讲 *** 技术的文章:https://developers.google.com/web/updates/2019/02/rendering-on-the-web You can easily prepare an app for server-side rendering using the Angular CLI. The CLI schematic @nguniversal/express-engine performs the required steps. 使用 Angular CLI,可以完成一个应用支持 *** 所需的准备工作,具体步骤通过 CLI Schematic 的 @nguniversal/express-engine 完成。 To create the server-side app module, app.server.module.ts, run the following CLI command. 为了创建面向服务器端渲染的 app module, 即 app.server.module.ts, 执行下列 CLI 指令: ng add @nguniversal/express-engine 该指令会创建下列的文件结构: To start rendering your app with Universal on your local system, use the following command. 本地使用 Universal 方式渲染应用的命令:npm run dev:*** 执行的是 package.json scripts 区块里定义的该命令: 如果遇到错误消息:An unhandled exception occurred: Cannot find module ‘@nguniversal/builders/package.json’Require stack: 先 npm install. 之后原始的错误消失。 A Node.js Express web server compiles HTML pages with Universal based on client requests. 一个 node.js express web 服务器,基于 Universal 编译 HTML 页面。 进行了很多编译动作: 整个 dist 文件夹和其子文件夹都是 npm run dev:*** 后自动生成的: 打开 http://localhost:4200/dashboard,看到了熟悉的 hero dashboard: Navigation via routerLinks works correctly because they use the native anchor () tags. 基于 routerLinks 的跳转可以正常工作,因为其使用原生的 a 标签。 使用 *** 的三大原因 Facilitate web crawlers through search engine optimization (SEO) 为了配合网络爬虫,实现搜索引擎优化。 Improve performance on mobile and low-powered devices 改善应用在移动端和低配设备上访问的性能。 Show the first page quickly with a first-contentful paint (FCP) 能够以 FCP 的方式,快速显示应用首页。 Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web. 谷歌,Bing,Facebook 和其他社交媒体网站,都使用网络爬虫,为应用内容建立索引,以便让其内容能够在网络上被搜索到。 These web crawlers may be unable to navigate and index your highly interactive Angular application as a human user could do. 如果一个 Angular 应用具备高度的可交互性,那么网络爬虫可能很难像一个人类用户一样,采用导航的方式访问应用,并索引其内容。 Angular Universal can generate a static version of your app that is easily searchable, linkable, and navigable without JavaScript. Angular Universal 可以基于 Angular 应用,生成一个静态版本,易于被搜索,链接,以及不借助 JavaScript 进行导航。 Angular Universal Universal also makes a site preview available since each URL returns a fully rendered page. 同时也能让 site 预览成为可能,因为每个 url 返回的是完全渲染过后的页面。 Some devices don’t support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. 有些设备不支持 JavaScript,或者支持程度很差,因此应用用户体验很难接受。 For these cases, you may require a server-rendered, no-JavaScript version of the app. 在这种情况下,我们需要一个服务器端渲染,不需要 JavaScript 也能运行的应用。 This version, however limited, may be the only practical alternative for people who otherwise couldn’t use the app at all. 这种版本的应用,虽然功能局限,但是总比完全不能用的版本好。 Show the first page quickly Displaying the first page quickly can be critical for user engagement. 为了确保用户体验,迅速显示应用首屏页面的能力至关重要。 Your app may have to launch faster to engage these users before they decide to do something else. With Angular Universal, you can generate landing pages for the app that look like the complete app. The pages are pure HTML, and can display even if JavaScript is disabled. 使用 Angular Universal,可以生成应用的初始页面,该页面和完整的应用相比外观上无区别,并且是纯粹的 HTML 代码,即使在 JavaScript 禁掉的浏览器上,也能正常显示。 The pages don’t handle browser events, but they do support navigation through the site using routerLink. 该页面不支持浏览器事件,但支持基于 routerLink 的 site 间导航。 In practice, you’ll serve a static version of the landing page to hold the user’s attention. 从操作层面说,我们可以提供应用初始页面的静态版本,以吸引用户的注意。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册