从MVC原理开始手敲一个MVC框架,带你体会当大神的乐趣
ccwgpt 2024-09-13 16:17 107 浏览 0 评论
每一个程序员,在刚入行时学的第一个框架应该就是ssm了,其他的各种学习框架都是后期在工作中,随着业务的不断复杂,在工作和bug中不断成长起来的,但是最经典的应该还是最一开始的时候ssm框架吧
当时刚学习这个时候,就觉得,好牛啊,这样就可以实现一个网站,这群大神是怎么做到的啊,嘿嘿嘿,不知道大家当时有没有这样的问题产生,所以今天就带大家来搭建一个简单的mvc框架,从原理讲起,也能帮助大家更好的理解底层源码
好了,话不多说,我们来看一下
Springmvc基本原理流程
SpringMvc本质上就是对Servlet的封装。
因为创建一个Maven项目,然后在pom文件中增加一个依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的-->
<scope>provided</scope>
</dependency>
2,创建DispatcherServlet,并注册到web.xml中
package com.dxh.edu.mvcframework.servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DxhDispatcherServlet extends HttpServlet {
/**
* 接收处理请求
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
web.xml:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>dxhmvc</servlet-name>
<servlet-class>com.dxh.edu.mvcframework.servlet.DxhDispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>springmvc.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dxhmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
2,注解开发
因为要使用到注解,所以首先要自定义几个注解:
这里就不赘述如何自定义注解了,详情请看:https://www.cnblogs.com/peida/archive/2013/04/24/3036689.html
Controller注解:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DxhController {
String value() default "";
}
Service注解:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DxhService {
String value() default "";
}
RequestMapping注解:
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DxhRequestMapping {
String value() default "";
}
Autowired注解:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DxhAutowired {
String value() default "";
}
编写测试代码:
测试代码我们放在同项目中的com.dxh.demo包中:
package com.dxh.demo.controller;
import com.dxh.demo.service.IDemoService;
import com.dxh.edu.mvcframework.annotations.DxhAutowired;
import com.dxh.edu.mvcframework.annotations.DxhController;
import com.dxh.edu.mvcframework.annotations.DxhRequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@DxhController
@DxhRequestMapping("/demo")
public class DemoController {
@DxhAutowired
private IDemoService demoService;
/**
* URL:/demo/query
*/
@DxhRequestMapping("/query")
public String query(HttpServletRequest request, HttpServletResponse response, String name){
return demoService.get(name);
}
}
package com.dxh.demo.service;
public interface IDemoService {
String get(String name);
}
package com.dxh.demo.service.impl;
import com.dxh.demo.service.IDemoService;
import com.dxh.edu.mvcframework.annotations.DxhService;
@DxhService("demoService")
public class IDemoServiceImpl implements IDemoService {
@Override
public String get(String name) {
System.out.println("Service实现类中的Name:"+ name);
return name;
}
}
目录结构:
3,编写自定义DispatcherServlet中的初始化流程:
在创建好的DxhDispatcherServlet中重写init()方法,并在init方法中做初始化配置:
- 加载配置文件 springmvc.properties
- 扫描相关的类——扫描注解
- 初始化Bean对象(实现IOC容器,基于注解)
- 实现依赖注入
- 构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系
@Override
public void init(ServletConfig config) throws ServletException {
//1. 加载配置文件 springmvc.properties
String contextConfigLocation = config.getInitParameter("contextConfigLocation");
doLoadConfig(contextConfigLocation);
//2. 扫描相关的类——扫描注解
doScan("");
//3. 初始化Bean对象(实现IOC容器,基于注解)
doInstance();
//4. 实现依赖注入
doAutoWired();
//5. 构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系
initHandleMapping();
System.out.println("MVC 初始化完成");
//6. 等待请求进入处理请求
}
以及5个空方法,这篇文章自定义MVC框架其实就是需要对这5个步骤的编写。
//TODO 5,构造一个映射器
private void initHandleMapping() {
}
//TODO 4,实现依赖注入
private void doAutoWired() {
}
//TODO 3,IOC容器
private void doInstance() {
}
//TODO 2,扫描类
private void doScan(String scanPackage) {
}
//TODO 1,加载配置文件
private void doLoadConfig(String contextConfigLocation) {
}
3.1 加载配置文件
- 首先在resource目录中创建一个配置文件——springmvc.properties
表示要扫描com.dxh.demo下的所有注解。 - 然后在web.xml中进行配置:
这样,就可以通过config.getInitParameter("contextConfigLocation")获得这个路径。
- 在DxhDispatcherServlet中定义一个属性,我们把加载后的配置文件中的信息,存储在Properties 中
private Properties properties = new Properties();;
//1,加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//根据指定路径加载成流:
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
}
3.2 扫描相关的类,扫描注解
- 上一步骤,我们已经把需要扫描的包存在Properties中,所以这里需要取出:
//2. 扫描相关的类——扫描注解
doScan(properties.getProperty("scanPackage"));
- 把扫描到的类型的全类名存在一个List中缓存起来,等待使用,在DxhDispatcherServlet中定义一个list:
//缓存扫描到的类的全类名
private List<String> classNames = new ArrayList<>();
- 从配置文件中我们得到了一个需要扫描的包名(com.dxh.demo),我们需要根据classPath+包名,来得到它实际上在磁盘上存的路径,然后递归,直到把所有的该包下(包括子包...)所有的类文件(.class结尾)。然后存在在List classNames中。
//2,扫描类
//scanPackage :com.dxh.demo package--->磁盘的文件夹(File)
private void doScan(String scanPackage) {
//1.获得classPath路径
String clasPath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
//2.拼接,得到scanPackage在磁盘上的路径
String scanPackagePath= clasPath + scanPackage.replaceAll("\\.","/");
File pack = new File(scanPackagePath);
File[] files = pack.listFiles();
for (File file : files) {
if (file.isDirectory()){ //子 package
//递归
doScan(scanPackage+"."+file.getName()); //com.dxh.demo.controller
}else if(file.getName().endsWith(".class")){
String className = scanPackage + "." + file.getName().replace(".class", "");
classNames.add(className);
}
}
}
3.3 初始化Bean对象(实现IOC容器,基于注解)
上一步骤我们把扫描到的类的全类名放到了,list中,那么本次步骤需要遍历整个list:
- 遍历List,依次得到所有的全类名
- 通过反射得到类对象
- 根据类对象判断有没有注解,并区分controller和servicecontroller,它的id此处不做过多处理,不取value了,用类的首字母小写作为id,保存到IOC容器中。service,service层往往是有接口的,再以接口名为id再存入一份bean到ioc,便于后期根据接口类型注入
- 完成
代码实现:
//IOC容器
private Map<String,Object> ioc = new HashMap<>();
//3,IOC容器
//基于classNames缓存的类的全限定类名,以及反射技术,完成对象创建和管理
private void doInstance() {
if (classNames.size()==0) return;
try{
for (int i = 0; i < classNames.size(); i++) {
String className = classNames.get(i); //com.dxh.demo.controller.DemoController
//反射
Class<?> aClass = Class.forName(className);
//区分controller ,区分service
if (aClass.isAnnotationPresent(DxhController.class)){
//controller的id此处不做过多处理,不取value了,用类的首字母小写作为id,保存到IOC容器中
String simpleName = aClass.getSimpleName();//DemoController
String lowerFirstSimpleName = lowerFirst(simpleName); //demoController
Object bean = aClass.newInstance();
ioc.put(lowerFirstSimpleName,bean);
}else if (aClass.isAnnotationPresent(DxhService.class)){
DxhService annotation = aClass.getAnnotation(DxhService.class);
//获取注解的值
String beanName = annotation.value();
//指定了id就以指定的id为准
if (!"".equals(beanName.trim())){
ioc.put(beanName,aClass.newInstance());
}else{
//没有指定id ,首字母小写
String lowerFirstSimpleName = lowerFirst(aClass.getSimpleName());
ioc.put(lowerFirstSimpleName,aClass.newInstance());
}
//service层往往是有接口的,再以接口名为id再存入一分bean到ioc,便于后期根据接口类型注入
Class<?>[] interfaces = aClass.getInterfaces();
for (Class<?> anInterface : interfaces) {
//以接口的类名作为id放入。
ioc.put(anInterface.getName(),aClass.newInstance());
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
3.4 实现依赖注入:
上一步骤把所有需要加载的bean,存入了ioc Map中,此时,我们就需要遍历这个map然后依次得到每个bean对象,然后判断对象中有没有被@****DxhAutowired修饰的属性。
- 遍历ioc这个map,得到每个对象
- 获取对象的字段(属性)信息
- 判断字段是否被@DxhAutowired修饰
- 判断@DxhAutowired有没有设置value值有,直接从ioc容器中取出,然后设置属性。无,需要根据当前字段的类型注入(接口注入)
代码实现:
//4,实现依赖注入
private void doAutoWired() {
if (ioc.isEmpty()){return;}
//1,判断容器中有没有被@DxhAutowried注解的属性,如果有需要维护依赖注入关系
for (Map.Entry<String,Object> entry: ioc.entrySet()){
//获取bean对象中的字段信息
Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
if (!declaredField.isAnnotationPresent(DxhAutowired.class)){
continue;
}
//有该注解:
DxhAutowired annotation = declaredField.getAnnotation(DxhAutowired.class);
String beanName = annotation.value(); //需要注入的bean的Id
if ("".equals(beanName.trim())){
//没有配置具体的beanId,需要根据当前字段的类型注入(接口注入) IDemoService
beanName = declaredField.getType().getName();
}
//开启赋值
declaredField.setAccessible(true);
try {
//字段调用,两个参数:(哪个对象的字段,传入什么)
declaredField.set(entry.getValue(),ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
3.5 构造一个handleMapping处理器映射器
构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系****。
手写MVC框架最关键的环节
假设有一个:
那么如何通过/demo/query定位到 DemoController类中的query这个方法 ?
之前我们所有被@DxhController(自定义Controller注解)的类,都存在了ioc 这个map中。
我们可以遍历这个map,得到每个bean对象
然后判断是否被@DxhController所修饰(排除@DxhService所修饰的bean)
然后判断是否被@DxhRequestMapping所修饰,有的话,就取其value值,作为baseUrl
然后遍历该bean对象中的所有方法,得到被@DxhRequestMapping修饰的方法。得到其value值,作为methodUrl。
baseUrl + methodUrl = url
我们把url和当前method绑定起来,存在map中,也就是建立了url和method建立映射关系。
代码实现:
//handleMapping ,存储url和method直接的映射关系
private Map<String,Object> handleMapping = new HashMap<>();
//5,构造一个映射器,将url和method进行关联
private void initHandleMapping() {
if (ioc.isEmpty()){return;}
for (Map.Entry<String,Object> entry: ioc.entrySet()){
//获取ioc中当前遍历对象的class类型
Class<?> aClass = entry.getValue().getClass();
//排除非controller层的类
if (!aClass.isAnnotationPresent(DxhController.class)){
continue;
}
String baseUrl = "";
if (aClass.isAnnotationPresent(DxhRequestMapping.class)){
//Controller层 类上 注解@DxhRequestMapping中的value值
baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value();
}
//获取方法
Method[] methods = aClass.getMethods();
for (Method method : methods) {
//排除没有@DxhRequestMapping注解的方法
if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;}
//Controller层 类中方法上 注解@DxhRequestMapping中的value值
String methodUrl = method.getAnnotation(DxhRequestMapping.class).value();
String url = baseUrl+methodUrl;
//建立url和method之间的映射关系,用map缓存起来
handleMapping.put(url,method);
}
}
}
4,测试一下:
到目前位置,还没有完全写完,但是不妨碍我们测试一下看看刚才写的那部分内容有没有什么问题:
完整的pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dxh.edu</groupId>
<artifactId>mvc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mvc Maven Webapp</name>
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的-->
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugins>
<!-- 编译插件定义编译细节-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
<!-- 告诉编译器,编译的时候记录下形参的真实名称-->
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
pom文件中加入了一个tomcat插件,并设置端口为8080,因此我们通过tomcat启动项目:
启动完成后,打开浏览器url中输入:
http://localhost:8080/demo/query
浏览器中什么都没返回(我们的代码还没真正的完成,尚未编写处理请求步骤),同时控制台中打印了MVC初始化完成,可以认为,目前的代码没有明显的缺陷。 我们继续~~~~~
5,改造initHandleMapping()
5.1 为什么改造?
DxhDispatcherServlet这个类继承了HttpServlet,并重写了doGet和doPost方法,在doGet中调用了doPost方法,当我们使用反射调用方法时(method.invoke(......))发现少了一部分参数:
因此我们要改造initHandleMapping(),修改url和method的映射关系(不简简单单的存入map中)。
5.2 新建Handler类
package com.dxh.edu.mvcframework.pojo;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
public class Handler {
//method.invoke(obj,)需要
private Object controller;
private Method method;
//spring中url支持正则
private Pattern pattern;
//参数的顺序,为了进行参数绑定 ,Key 参数名, Value 代表第几个参数
private Map<String,Integer> paramIndexMapping;
public Handler(Object controller, Method method, Pattern pattern) {
this.controller = controller;
this.method = method;
this.pattern = pattern;
this.paramIndexMapping = new HashMap<>();
}
//getset方法这里省略,实际代码中需要...
}
在Handler类中编写了4个属性:
- private Object controller:method.invoke(obj,)需要
- private Method method:与url绑定的方法
- private Pattern pattern:可以通过正则匹配,也可以直接写String url。
- private Map<String,Integer> paramIndexMapping:参数的顺序,为了进行参数绑定 ,Key 参数名, Value 代表第几个参数
5.3 修改initHandleMapping()
首先,就不能直接通过Map<url,Method>的得方式进行关系映射了,使用一个list,泛型是刚才创建的Handler。
//handleMapping ,存储url和method直接的映射关系
// private Map<String,Method> handleMapping = new HashMap<>();
private List<Handler> handlerMapping = new ArrayList<>();
改动前,改动后代码对比:
改动后的initHandleMapping():
//5,构造一个映射器,将url和method进行关联
private void initHandleMapping() {
if (ioc.isEmpty()){return;}
for (Map.Entry<String,Object> entry: ioc.entrySet()){
//获取ioc中当前遍历对象的class类型
Class<?> aClass = entry.getValue().getClass();
//排除非controller层的类
if (!aClass.isAnnotationPresent(DxhController.class)){
continue;
}
String baseUrl = "";
if (aClass.isAnnotationPresent(DxhRequestMapping.class)){
//Controller层 类上 注解@DxhRequestMapping中的value值
baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value();
}
//获取方法
Method[] methods = aClass.getMethods();
for (Method method : methods) {
//排除没有@DxhRequestMapping注解的方法
if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;}
//Controller层 类中方法上 注解@DxhRequestMapping中的value值
String methodUrl = method.getAnnotation(DxhRequestMapping.class).value();
String url = baseUrl+methodUrl;
//把method所有信息以及url封装为Handler
Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url));
//处理计算方法的参数位置信息
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//不做太多的参数类型判断,只做:HttpServletRequest request, HttpServletResponse response和基本类型参数
if (parameter.getType()==HttpServletRequest.class||parameter.getType()==HttpServletResponse.class){
//如果时request和response对象,那么参数名称存 HttpServletRequest 和 HttpServletResponse
handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i);
}else{
handler.getParamIndexMapping().put(parameter.getName(),i);
}
}
handlerMapping.add(handler);
}
}
}
6, 请求处理开发 doPost():
上一步骤,我们配置了 uri和method的映射关系,并封装到了Handler中存入list,那么接下来,就要通过HttpServletRequest,取出uri,然后找到具体的Handler:
- 通过HttpServletRequest取出uri找到具体的Handler
- 得到将调用方法的参数的数组
- 根据上述数组长度创建一个新的数组(参数数组,传入反射调用的)
- 通过req.getParameterMap()得到前台传来的参数parameterMap
- 遍历parameterMap
- 通过StringUtils.join方法把name=1&name=2格式的参数变为name[1,2] (需要commons-lang依赖)
- 参数匹配并设置
private Handler getHandler(HttpServletRequest req) {
if (handlerMapping.isEmpty()){return null;}
String url = req.getRequestURI();
//遍历 handlerMapping
for (Handler handler : handlerMapping) {
Matcher matcher = handler.getPattern().matcher(url);
if (!matcher.matches()){continue;}
return handler;
}
return null;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//根据uri获取到能够处理当前请求的Handler(从handlerMapping中(list))
Handler handler = getHandler(req);
if (handler==null){
resp.getWriter().write("404 not found");
return;
}
//参数绑定
//该方法所有参数得类型数组
Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
//根据上述数组长度创建一个新的数组(参数数组,传入反射调用的)
Object[] paramValues = new Object[parameterTypes.length];
//以下就是为了向参数数组中设值,而且还得保证参数得顺序和方法中形参顺序一致。
Map<String,String[]> parameterMap = req.getParameterMap();
//遍历request中所有的参数 ,(填充除了request、response之外的参数)
for (Map.Entry<String,String[]> entry: parameterMap.entrySet()){
//name=1&name=2 name[1,2]
String value = StringUtils.join(entry.getValue(), ",");// 如同 1,2
//如果参数和方法中的参数匹配上了,填充数据
if (!handler.getParamIndexMapping().containsKey(entry.getKey())){continue;}
//方法形参确实有该参数,找到它得索引位置,对应得把参数值放入paramValues
Integer index = handler.getParamIndexMapping().get(entry.getKey());
//把前台传递过来的参数值,填充到对应得位置去
paramValues[index] = value;
}
Integer requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());
paramValues[requestIndex] = req;
Integer responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());
paramValues[responseIndex] = resp;
//最终调用handler得method属性
try {
Object invoke = handler.getMethod().invoke(handler.getController(), paramValues);
//简单操作,把方法返回的数据以字符串的形式写出
resp.getWriter().write(invoke.toString());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
7,测试:
打开浏览器,url中输入:http://localhost:8080/demo/query?name=lisi
返回:
控制台中打印出:
OK完成~
相关推荐
- 如何让老师看完文章后还啧啧称奇?满分作文有框架,这3点是关键
-
历年来语文考试中,作文的分数都占着相当大的一个比例,同时作文也是最容易拉开差距的一个模块。别人拿满分,而你却只有20分左右,分数的差距就是这样拉开的。作文想拿很高的分数却是不容易,但不是完全不可能的事...
- 小学作文写作技巧和方法,万能公式框架法。家长收藏
-
小学语文老师用心整理,将写作框架编成万能公式。作文的写作是语文学习中的重要一环,从小学到初中再到高中,作文一直是语文考试中占分比重最高的部分。小学阶段的语文写作相对来说比较简单,主要是打基础,但是很多...
- 如何用爆款改写技巧提升文章吸引力结构重塑:打破原文框架悬念前
-
如何用爆款改写技巧提升文章吸引力?结构重塑:打破原文框架悬念前置法-在开头设置悬念或提出反常识的结论,吸引读者注意力。例如,将“接纳不完美是治愈的开始”改写为“天天逼自己当完美超人?别杠了!生活本就...
- 守护袁昆:是否有必要按框架去写文章,拍摄剪辑视频?
-
(文/守护袁昆)如今不管是写文章还是剪辑视频,越来越多的朋友喜欢用框架、用脚本,作为互联网创作者,我们是否有必要按框架去写文章,拍摄剪辑短视频呢?其实在内容创作过程中,是否使用框架始终是一个充满争议的...
- 揭秘!爆款文章的秘密:让读者无法抗拒的文章框架
-
说说我自己一开始写文章都会犯一个毛病,文章,通常是想到哪里,写到哪里,“管不住字儿”。这样往往会出现以下问题绊住我们继续写下去1、很容易,写着就跑偏了,最终出来的成品和最初的设想偏离很大2、会写得很慢...
- 想要写出逻辑清晰的文章,你需要掌握哪些写作结构
-
想要写出好文章,就必须要了解文章的结构和框架。一篇文章结构清晰,读者就很容易跟上作者的思路,看出文章的重点内容。如果你对新媒体的文章有进行过研究,你就会发现很多公众号的文章结构都是类似的。所以你需要掌...
- 写作结构拆解:从选题到框架,如何让文章说服力翻倍?
-
你有没有想过,为什么有些文章能轻松获得很高的阅读量,而你的文章却始终无人问津?其实,写出爆款文章并没有想象中那么难。关键在于选择一个吸引人的主题,并用一个清晰的写作框架,通过2-3个有力的子观点支撑...
- 写作总被吐槽逻辑混乱?三步搭建框架法,新手也能写出漂亮文章
-
一、结构决定论:信息传递的桥梁写作者和读者之间始终存在一道隐形的鸿沟。作者脑海中的想法如同一棵枝繁叶茂的大树,但直接倾倒给读者时,往往只剩下零散的枝叶,信息在传达过程中的丢失,作者输出的和读者读到的不...
- 如何搭建文章框架:新手写作者很有必要看
-
#头条深一度-深度阅读计划#见面好呀,我是潼臻~37岁,边上班边带娃的二胎妈妈藏起生活里的琐碎,期望你我都能遇到更好的自己~~~~~~~~~~~~~~~~~~果然多读书是可以真切学习到有用的东西最近把...
- 模型上下文协议(MCP)的可视化向导
-
最近,模型上下文协议(MCP)引起了广泛关注。你一定听说过它。今天,让我们来了解一下它是什么。直观地说,MCP就像是AI应用的USB-C接口。正如USB-C提供了一种标准化的方式,用于将...
- 97个人放一页PPT!用对Smartart架构图直接开挂!
-
从讯飞出差回来的路上,在高铁上看到一条微博,关于红楼梦人物的思维导图:下面很多家长说,帮助很大,能帮助上学的孩子梳理清人物关系,我看了下,清楚是清楚,但真的不太好看!作为一名PPT博主,我就顺带在高...
- 技术架构规范与实践(二)架构设计示例
-
1.逻辑架构1.1领域概念1.2宏观应用架构1.3宏观流程1.4微服务拆分与分层2.技术架构3.开发架构3.1后端技术栈分类名称版本描述后端框架/组件JavaJdk8(openjdk:8u342)后...
- 倾斜柱模板安装加固
-
1、适用范围:呈梯形逐层向内侧倾斜的框架柱。2、工艺流程:定位放线-配模-校正梁位置-安装加固。3、工艺方法:(1)定位放线首先现场技术管理人员对每颗不同标高的梁底、梁中边线及200mm控制线进行平面...
- 地基与基础工程、主体工程节点构造
-
#去班味吧#桩头凿除环切法工艺说明:1、根据桩头预留长度(深入承台10cm)放样桩顶标高,施工人员根据测量结果在基桩上用红油漆标注环切线;2、在切割线以上部分桩底,人工用钢钎打入桩底约15cm,打入时...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- 知识框架图 (52)
- ppt框架 (55)
- 框架图模板 (59)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)