Universal-Image-Loader在github上的地址:https://github.com/nostra13/Android-Universal-Image-Loader

它的基本使用请参考我的另一篇博客 http://www.cnblogs.com/yuan1225/p/8426900.html,下面我从源码角度研究它。

ImageLoader使用的是双重判断的懒汉试单例模式。

 /** Returns singleton class instance */
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}

先看ImageLoader的初始化过程:

 /**
* Initializes ImageLoader instance with configuration.<br />
* If configurations was set before ( {@link #isInited()} == true) then this method does nothing.<br />
* To force initialization with new configuration you should {@linkplain #destroy() destroy ImageLoader} at first.
*
* @param configuration {@linkplain ImageLoaderConfiguration ImageLoader configuration}
* @throws IllegalArgumentException if <b>configuration</b> parameter is null
*/
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}

以上是在Application中调用 ImageLoader.getInstance().init(config.build());的初始化过程。接下来是ImageLoader的使用过程分析

ImageLoader.getInstance().displayImage(url, imageView, options);
displayImage方法有很大重载的方法,最终都会辗转调用到最复杂的这个重载方法:
 public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
} if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
} if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); listener.onLoadingStarted(uri, imageAware.getWrappedView()); Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
} ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}

参数的意义:

1.URI uri : Image URI;可用的几种URI:

"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13" // from content provider
"content://media/external/video/media/13" // from content provider (video thumbnail)
"assets://image.png" // from assets
"drawable://" + R.drawable.img // from drawables (non-9patch images)
2.ImageAware imageAware : 显示图像的视图,是androidimageview的包装类,参数来源是new ImageViewAware(imageView).不能为空
3.DisplayImageOptions options :用于图像解码和显示,如果该参数为空则使用默认的option。
4.ImageSize targetSize:图像目标大小。 如果为空 - 大小将取决于视图。
5.ImageLoadingListener listener:用于图像加载过程监听。 如果在UI线程上调用此方法,则监听器在UI线程上触发事件
6.ImageLoadingProgressListener progressListener:图像加载进度监听。 如果在UI线程上调用此方法,则监听器在UI线程上触发事件。 应在{option选项}中启用缓存磁盘以使此侦听器正常工作。
这个方法比较长,它的逻辑比较清晰,主要做了下面的方法:
1.判断各个参数是否合法,是否需要默认值
判断配置参数和显示图片的控件是否为空,如果为空直接抛出了异常
判断listener options targetsize是否为null,如果为空则使用默认值
判断uri是否为空,如果uri为空,则在ImageLoaderEngine中取消该视图的显示任务,如果在options中设置了showImageForEmptyUri(R.drawable.ic_empty)则为该视图显示一个默认的空uri时的图片,直接返回。
2.开始监听下载任务。先从缓存中读取图片的bitmap,如果缓存中有则直接使用,否则需要从磁盘或者从网络下载图片。
下面就来看如何从缓存中读取,如何下载。
当从memoryCache读取的bitmap不为null 并且没有被回收时,就直接展示缓存中的这个bitmap。默认情况下options.shouldPostProcess()是false。除非在初始化options选项时设置了postProcesser。
所以我们之间看49行。点开display方法,咦,它是一个接口。
它有几个实现类分别实现不同的图片显示方法。如果在初始化options选项没有设置displayer()选项则默认使用SimpleBitmapDisplayer()正常显示一张图片。如果设置了如下
.displayer(new CircleBitmapDisplayer(Color.WHITE,5))则显示圆角图片。

下面以SimpleBitmapDisplayer为例,分析如何实现display的。

 public final class SimpleBitmapDisplayer implements BitmapDisplayer {
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
}

这个实现类比较简单,只有一个方法一句话,可以把它理解成我们在android中给imageView设置图片的一种方式:imageview.setImageBitmap(bitmap);

ImageViewAware是Android imageView的包装类,保持ImageView的弱引用以防止内存泄漏。如何使用imageview的弱引用这一步暂时忽略,先回到第34行。

以上是缓存中有bitmap,下面分析如果从缓存中获取的bitmap为空,则需要加载。因为Androidx.x之后不容许在UI线程中做网络加载的操作,所以我们只分析异步加载的方式,就是第66行engine.submit(displayTask);

在这里涉及到了一个非常重要的类:ImageLoaderEngine,它负责{LoadAndDisplayImageTask显示任务}的执行。下面重点解析一下ImageLoaderEngine这个类。
engine这个对象是什么时候初始化的呢?请回到Imageloader对象的初始化方法init中,第15行:engine = new ImageLoaderEngine(configuration);
看一下ImageLoaderEngine的构造方法:
 ImageLoaderEngine(ImageLoaderConfiguration configuration) {
this.configuration = configuration; taskExecutor = configuration.taskExecutor;
taskExecutorForCachedImages = configuration.taskExecutorForCachedImages; taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
}

以taskExecutorForCachedImages 为例,taskExecutorForCachedImages 是一个线程池,异步显示memory cache里面的bitmap。

进入submit方法:

     /** Submits task to execution pool */
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
15 });
}

当执行到第10行时,就会调用LoadAndDisplayImageTask的run方法,接下来看这个类。这个类的对象封装了engine和Imageloadinginfo,所以包含了所有的configuration 和options选项。

来看它的run方法

 @Override
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return; ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
} loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual(); bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired checkTaskNotActual();
checkTaskInterrupted(); if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
} if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
} if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
} DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}

在这里使用了可重入锁机制来保证并发操作时数据的完整性。先从缓存中获取到的bitmap==null,看19行,来看这个方法。

 private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual();
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK; String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
} checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding); if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
}
.... return bitmap;
}

在这个方法返回了一个解码后的bitmap,是从磁盘读取文件或者网络中获取的。获取到bitmap后就回到run方法的58行。这里将bmp, imageLoadingInfo, engine等封装了一个runnable的实现类DisplayBitmapTask中。

同样来看一下这个runnable的run方法。

 @Override
public void run() {
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}

第11行就回到了display方法,去给我们的ImageView设置bitmap。到此就完成了图片在控件中的展示过程。取消这次任务,并回调监听器的onLoadingComplete方法。

接下来我们来分析这个框架是如何加载图片的bitmap的。回到

tryLoadBitmap()方法的第10行或者25行,看decodeImage(imageUriForDecoding);这个方法
 private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}

图片的uri等信息被封装到了ImageDecodingInfo对象当中。其中getDownloader()返回了ImageDownloader这个接口实现类对象的引用。 那这个ImageDownloader的对象是从哪里来的呢?我们看LoadAndDisplayImageTask的构造方式中,

downloader = configuration.downloader;
networkDeniedDownloader = configuration.networkDeniedDownloader;
slowNetworkDownloader = configuration.slowNetworkDownloader;

而configuration 是最初在Application类中初始化  ImageLoader 的时候用户传递过来的参数。初始化ImageLoaderConfiguration.Builder时为其设置imageDownloader,如果不设置该变量,则在build()的时候在initEmptyFieldsWithDefaultValues方法中为其初始化一个默认值:

 if (downloader == null) {
downloader = DefaultConfigurationFactory.createImageDownloader(context);
}

其实也就是ImageDownloader的一个实现类的对象BaseImageDownloader对象

 public static ImageDownloader createImageDownloader(Context context) {
return new BaseImageDownloader(context);
}

而ImageDownloader对外提供了getStream方法,根据uri获取输入流信息。

 InputStream getStream(String imageUri, Object extra)

然后我们来看decodeImage方法中的decode做了什么。ImageDecoder是一个接口,里面只有一个decode方法,它的主要作用就是将获取到的InputStream转换成Bitmap。看它的实现类BaseImageDecoder

 public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo; InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
} if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//将Bitmap缩放和旋转成满足需求的Bitmap
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}

第5行,这个方法调用的就是ImageDownloader的 getStream,并根据uri的Scheme信息判断这个图片是在哪里,从这里真正去获取bitmap。

 @Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
return getStreamFromNetwork(imageUri, extra);
case FILE:
return getStreamFromFile(imageUri, extra);
case CONTENT:
return getStreamFromContent(imageUri, extra);
case ASSETS:
return getStreamFromAssets(imageUri, extra);
case DRAWABLE:
return getStreamFromDrawable(imageUri, extra);
case UNKNOWN:
default:
return getStreamFromOtherSource(imageUri, extra);
}
}

我们以网络获取为例进行分析:

 protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra); int redirectCount = 0;
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
conn = createConnection(conn.getHeaderField("Location"), extra);
redirectCount++;
} InputStream imageStream;
try {
imageStream = conn.getInputStream();
} catch (IOException e) {
// Read all data to allow reuse connection (http://bit.ly/1ad35PY)
IoUtils.readAndCloseStream(conn.getErrorStream());
throw e;
}
if (!shouldBeProcessed(conn)) {
IoUtils.closeSilently(imageStream);
throw new IOException("Image request failed with response code " + conn.getResponseCode());
} return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}

通过HttpURLConnection从服务器获取一个InputStream,封装在了一个自定义的ContentLengthInputStream中。获取到InputStream后返回到decode方法的11行.

 protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
throws IOException {
Options options = new Options();
options.inJustDecodeBounds = true; //true那么将不返回实际的bitmap对象,不给其分配内存空间但是可以得到一些解码边界信息即图片大小等信息  
BitmapFactory.decodeStream(imageStream, null, options); ExifInfo exif;
String imageUri = decodingInfo.getImageUri();
if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
exif = defineExifOrientation(imageUri);
} else {
exif = new ExifInfo();
}
return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
}

在这个方法中第一次调用 BitmapFactory.decodeStream(imageStream, null, options); 获取图片的大小解码边界等信息

decode方法的第12行:是重新获取bitmap,这个我也没有搞清楚它为什么又获取一遍,可能是因为调用过来一次decodeStream,但是第一次调用只是为了得到图片的一些信息,并未得到bitmap,所以需要重新对一个新的inputStream进行decode,
只是这个时候已经知道了图片的大小 格式等信息了。就可以直接返回bitmap。 至此,过去bitmap的过程已经完毕,这就是display时调用的imageAware.setImageBitmap(bitmap);中的bitmap的由来。

Universal-Image-Loader源码解解析---display过程 + 获取bitmap过程的更多相关文章

  1. netty源码解解析(4.0)-11 Channel NIO实现-概览

      结构设计 Channel的NIO实现位于io.netty.channel.nio包和io.netty.channel.socket.nio包中,其中io.netty.channel.nio是抽象实 ...

  2. netty源码解解析(4.0)-10 ChannelPipleline的默认实现--事件传递及处理

    事件触发.传递.处理是DefaultChannelPipleline实现的另一个核心能力.在前面在章节中粗略地讲过了事件的处理流程,本章将会详细地分析其中的所有关键细节.这些关键点包括: 事件触发接口 ...

  3. netty源码解解析(4.0)-17 ChannelHandler: IdleStateHandler实现

    io.netty.handler.timeout.IdleStateHandler功能是监测Channel上read, write或者这两者的空闲状态.当Channel超过了指定的空闲时间时,这个Ha ...

  4. netty源码解解析(4.0)-18 ChannelHandler: codec--编解码框架

    编解码框架和一些常用的实现位于io.netty.handler.codec包中. 编解码框架包含两部分:Byte流和特定类型数据之间的编解码,也叫序列化和反序列化.不类型数据之间的转换. 下图是编解码 ...

  5. netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端

    本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...

  6. netty源码解解析(4.0)-4 线程模型-概览

    netty线程体系概览 netty的高并发能力很大程度上由它的线程模型决定的,netty定义了两种类型的线程: I/O线程: EventLoop, EventLoopGroup.一个EventLoop ...

  7. netty源码解解析(4.0)-14 Channel NIO实现:读取数据

     本章分析Nio Channel的数据读取功能的实现. Channel读取数据需要Channel和ChannelHandler配合使用,netty设计数据读取功能包括三个要素:Channel, Eve ...

  8. netty源码解解析(4.0)-15 Channel NIO实现:写数据

    写数据是NIO Channel实现的另一个比较复杂的功能.每一个channel都有一个outboundBuffer,这是一个输出缓冲区.当调用channel的write方法写数据时,这个数据被一系列C ...

  9. netty源码解解析(4.0)-13 Channel NIO实现: 关闭和清理

    Channel提供了3个方法用来实现关闭清理功能:disconnect,close,deregister.本章重点分析这个3个方法的功能的NIO实现. disconnect实现: 断开连接 disco ...

随机推荐

  1. tar结果find打包指定后缀的文件

    find 目录名 -name "*.ini" | xargs tar czvf tarch.tar.gz  tar czf tmp.tar.gz tmp/ --exclude=&q ...

  2. 移动端调试技巧(禁止webviuew,inspect等)

    如果由于某种原因(天朝FQ),不能支持google 的 inspect  调试 或者再想在某个APP里面调试你的页面,但是没有打开APP的webview ,也不能授权调试 在或者,Fider 可以拦截 ...

  3. ACCA AI来袭会议笔记

    ACCA AI来袭会议笔记 Technology in Accounting 调研报告: http://cn.accaglobal.com/news/professional_report.html ...

  4. Ng1从1.3开始的变更史

    从今有个ng1 spa项目,项目可能会有ng1的版本升级问题,特简要摘录从1.3的主要版本变更,所以内容来自migration guide. 1.3的主要变更: 1.controller不能再以全局简 ...

  5. Application "org.eclipse.ui.ide.workbench" could not be found in the registry.问题的解决

    今天升级Eclipse,升级完Restart,碰到启动不了让看日志,日志里主要错误信息即是Application "org.eclipse.ui.ide.workbench" co ...

  6. The note of Developing Innovative Ideas for New Companies Course

    This course is free on the Coursera Site,But it only has English version Threee pieces of the course ...

  7. Reactor三种线程模型与Netty线程模型

    文中所讲基本都是以非阻塞IO.异步IO为基础.对于阻塞式IO,下面的编程模型几乎都不适用 Reactor三种线程模型 单线程模型 单个线程以非阻塞IO或事件IO处理所有IO事件,包括连接.读.写.异常 ...

  8. 填坑!!!virtualenv 中 nginx + uwsgi 部署 django

    一.为什么会有这篇文章 第一次接触 uwsgi 和 nginx ,这个环境搭建,踩了太多坑,现在记录下来,让后来者少走弯路. 本来在 Ubuntu14.04 上 搭建好了环境,然后到 centos7. ...

  9. Flask构建微电影(二)

    第三章.项目分析.搭建目录及模型设计 3.1.前后台项目目录分析 微电影网站 前台模块 后台模块 前台(home) 数据模型:models.py 表单处理:home/forms.py 模板目录:tem ...

  10. vue 路由嵌套高亮问题

    正常路由嵌套是没有问题的,但是如果你已经在当前主路由页面了,然后再次点击主路由就会出现页面数据空白的情况 看代码: //主路由通过v-for循环出来 <div class="list- ...