专栏首页李蔚蓬的专栏异步网络下载案例

异步网络下载案例

异步网络下载案例(AsyncTask + 前台Service + OkHttp + Android8.0的Notification适配注意)

ServiceBestPractice项目(模块)GitHub地址

案例代码逻辑概述

  • interface DownloadListener 回调机制核心接口
  • class DownloadTask extends AsyncTask<String, Integer, Integer> 描述异步网络下载逻辑(网络请求,文件线上状态处理,文件本地状态处理,文件写入本地), 抽象调用接口对象方法;
  • class DownloadService extends Service
    • 匿名类方式具体实现回调接口的方法 而后将这个匿名类放入一个接口类实例中 (回调方法负责状态处理,方式是:Toast、对downloadTask归为、开关通知等);(Binder类定义中)
    • 实例化DownloadTask,把实现好的接口类实例传进去DownloadTask的构造器; 为外部(如Activity)调用准备好业务Binder实例class DownloadBinder extends Binder
    • 封装NotificationManager以及NotificationgetNotificationManager() getNotification(String title, int progress)
    • Binder类定义中执行DownloadTask实例downloadTask.execute(downloadUrl); 并完成开关通知、删除文件逻辑;

getNotificationManager();// 配置 NotificationManager!!!!!!!! startForeground(1, getNotification("Downloading...", 0)); ------------------------------------ file.delete(); ... getNotificationManager().cancel(1); stopForeground(true);

  • class MainActivity extends AppCompatActivity implements View.OnClickListener
    • 实例化UI(主要是按钮);
    • 启动、绑定、解绑服务;startForegroundService(intent); startService(intent); unbindService(connection);
    • 运行时权限动态申请;
    • 准备监听事件,事件中通过Servicebinder对象来产生业务;

开始实战

  • 创建ServiceBestPractice项目或模块。 首先在/build.gradle中dependencies下添加OKHttp库依赖(网络相关功能使用):
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
  • 运用回调机制编程, 定义一个回调接口, 用于对下载过程中的各种状态进行监听和回调: (在DownloadTask的onPostExecute中抽象调用, 在DownloadService中具体实现)
public interface DownloadListener {
    void onProgress(int progress);//通知当前下载进度
    void onSuccess();//通知下载成功事件
    void onFailed();//通知下载失败事件
    void onPaused();//通知下载暂停事件
    void onCanceled();//通知下载取消事件
}
  • 编写下载功能,新建DownloadTask类继承自AsyncTask
/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/11/9 17:29
 *     desc   :三个泛型参数,
 *     第一个表示在执行AsyncTask时需传入一个字符串参数给后台任务,
 *     第二个使用整型数据最为进度显示单位,
 *     第三个表示使用整型数据来反馈结果执行
 * </pre>
 */
public class DownloadTask extends AsyncTask<String, Integer, Integer> {

    //定义四个整型常量分别表示下载的不同状态
    public static final int TYPE_SUCCESS = 0;//表示下载取消
    public static final int TYPE_FAILED = 1;//表示下载失败
    public static final int TYPE_PAUSE = 2;//表示下载暂停
    public static final int TYPE_CANCELED = 3;//表示下载取消

    private DownloadListener listener;

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    private boolean isCanceled = false;
    private boolean isPaused = false;

    private int lastProgress;//记录上次的进度

    //构造方法
    public DownloadTask(DownloadListener listener){
        //将下载的状态通过此参数进行回调,此处负责调用,外部具体编写逻辑
        this.listener = listener;
    }

    //在后台执行具体的下载逻辑
    // String... params:可变长参数列表,必须是String类型,转化为数组处理
    @Override
    protected Integer doInBackground(String... params) {

        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;

        try{

            long downloadedLength = 0;//记录 已下载的文件 长度!!!!!!!

            String downloadUrl = params[0];//获取 下载的URL地址!!!!!!!!!

            // 根据URL地址解析出下载的文件名
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            // 指定文件下载到 Environment.DIRECTORY_DOWNLOADS 目录下,即SD卡的Download目录
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            //用以上的 文件下载路径 以及 要下载的文件名 得到 file句柄!!!!!!!!!!!!
            file = new File(directory + fileName);


            //判断是否已存在要下载的文件,
            // 存在则 读取 已下载的字节数(以 启用 断点续传 功能)
            if (file.exists()){
                downloadedLength = file.length();
            }


            //获取 待下载文件 的总长度!!!!!!
            // 判断 文件情况—— 有问题 或者 已下载完毕!!!!!
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0){//总长度为0,说明文件有问题
                return TYPE_FAILED;

            }else if (contentLength == downloadedLength){//已下载字节和文件总字节相等,说明已经下载完成了
                return TYPE_SUCCESS;

            }


            //注意这里,断点续传 功能!!!!!!!!!!
            //使用.addHeader 往请求中添加一个Header,用于告诉服务器我们想要
            // 从哪个字节开始下载(已下载部分不需再重新下载)
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();//得到服务器响应的数据

            //使用 Java文件流方式 不断从网络上 读取数据!!
            // 不断写入到本地,
            // 直到文件全部下载完为止!!
            if (response != null){

                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");//封装本地文件句柄
                savedFile.seek(downloadedLength);//跳过已下载的字节


                byte[] b = new byte[1024];
                int total = 0; //本轮!!!下载的总长度!!
                int len;

                //使用 Java文件流方式 不断从网络上 读取数据!!
                // 不断写入到本地,直到文件全部下载完为止!!
                while ((len = is.read(b)) != -1){

                    //判断用户有没触发暂停或取消操作,如果有则返回相应值来中断下载
                    if (isCanceled){
                        return TYPE_CANCELED;

                    }else if (isPaused){
                        return TYPE_PAUSE;


                    }else {

                        //用户没有触发暂停或取消操作,继续下载
                        total += len;
                        savedFile.write(b, 0, len);

                        //计算已下载的百分比 == (本轮下载的长度 + 已经下载的长度)/ 要下载的 文件总长度
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);

                        publishProgress(progress);//抛出进度给 onProgressUpdate(),回调之!!!!
                    }
                }

                //执行到此,说明以上循环已执行完毕,文件下载完毕
                response.body().close();

                return TYPE_SUCCESS;

            }
        } catch (Exception e) {
            e.printStackTrace();

        }finally {

            //分开关闭资源!!!!!!
            try {

                if (is != null){
                    is.close();
                }

                if (savedFile != null){
                    savedFile.close();
                }

                if (isCanceled && file != null){
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //不从上面成功退出则执行至此,证明失败!!!
        return TYPE_FAILED;
    }

    /**
     * 在界面更新当前的下载进度
     *
     * doInBackground()的每一次!!!while 读 输入流 ,
     * 写入file,都会publishProgress(progress); 抛出进度
     * 此时就会回调此方法!!! 对进度进行处理!!!
     *
     * @param values
     */
    @Override
    protected void onProgressUpdate(Integer... values) {

        //获取当前下载进度,
        // 参数来自 doInBackground()中 publishProgress()抛出的进度
        int progress  = values[0];

        if (progress > lastProgress){//与上一次下载进度对比

            listener.onProgress(progress);//有变化则调用DownloadListener的onProgress()通知下载进度更新

            lastProgress = progress;//更新记录
        }
    }

    /**
     *  通知最终的下载结果
     *
     * 当任务执行完了,即doInBackground()一旦return,
     * 其return的值就会传到这里,作为参数,
     * 参数类型即定义泛型时的第三个参数
     *
     * 这里用了回调机制,listener负责抽象调用!!!
     * 外部负责具体实现!!!
     */
    @Override
    protected void onPostExecute(Integer status) {
        switch (status){//根据传入的下载状态进行回调
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;

            case TYPE_FAILED:
                listener.onFailed();
                break;

            case TYPE_PAUSE:
                listener.onPaused();
                break;

            case TYPE_CANCELED:
                listener.onCanceled();
                break;

            default:
                break;
        }
    }

    //取消位以及暂停位
    // 由外部调用,在doInBackground()中生效
    public void pauseDownload(){
        isPaused = true;
    }
    public void cancelDownload(){
        isCanceled = true;
    }

    private long getContentLength(String downloadUrl) throws IOException {
        //请求得到需下载的文件
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(downloadUrl).build();
        Response response = client.newCall(request).execute();

        //得到文件长度
        if (response != null && response.isSuccessful()){
            long contentLength = response.body().contentLength();
            response.close();

            return contentLength;
        }
        return 0;
    }

}

普及:关于RandomAccessFile Java除了File类之外,还提供了专门处理文件的类, 即RandomAccessFile(随机访问文件)类。 该类是Java语言中功能最为丰富的文件访问类, 它提供了众多的文件访问方法。 RandomAccessFile类支持“随机访问”方式, 这里“随机”是指可以跳转到文件的任意位置处读写数据。 在访问一个文件的时候,不必把文件从头读到尾, 而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分, 这时使用RandomAccessFile类就是最佳选择。 RandomAccessFile对象类有个位置指示器,指向当前读写处的位置, 当前读写n个字节后,文件指示器将指向这n个字节后面的下一个字节处 刚打开文件时,文件指示器指向文件的开头处, 可以移动文件指示器到新的位置,随后的读写操作将从新的位置开始。 RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势, 但该类仅限于操作文件, 不能访问其他的I/O设备,如网络、内存映像等;

  • 专门处理文件的类------RandomAccessFile类
  • RandomAccessFile对象, 当前读写(read/write)n个字节后, 文件指示器将自动指向这n个字节后面的下一个字节处 RandomAccessFile是面向文件(file对象)的,可以用来读写本地SD、硬盘; BufferReader、BufferWriter也有类似的指示器 使用readline()write()读写(read/write)n个字节后, 指示器将自动指向这n个字节后面的下一个字节处 只不过BufferReader、BufferWriter面向的是IO流。
  • 为了保证DownloadTask可一直在后台运行, 需创建一个下载的服务DownloadService
public class DownloadService extends Service {

    private DownloadTask downloadTask;
    private String downloadUrl;

    private String notificationId = "nyd001";
    private String notificationName = "downloadTask";

    /**
     * 创建DownloadListener 匿名内部类实例,
     * 然后赋值给其父类类型DownloadListener引用
     *
     * 这里实现的方法!!
     * 直接在DownloadTask 的 onPostExecute()中被调用
     *
     * 而onPostExecute() 中要调用那个回调方法
     *
     * 则由doInBackground() 的返回值位决定
     *
     * 而doInBackground() 的返回值 中
     * 成功位 和 失败位 是 客观判断的结果
     * 暂停位 和 取消位 可以 由人为点击置位
     */
    private DownloadListener listener = new DownloadListener() {

        /**
         * 在 DownloadTask 中的 onProgressUpdate()处调用
         * @param progress 来自对应的DownloadTask 的 doInBackground() 中的 publishProgress(progress);
         */
        @Override
        public void onProgress(int progress) {
            //getNotification()是自定义的封装方法,
            // 其中构造了一个用于显示下载进度的通知,
            //调用NotificationManager的 notify() 去触发这个通知,
            // 这样就可以在下拉状态栏中实时看到当前的下载进度了
            getNotificationManager().notify(1, getNotification("Downloading...", progress));
        }

        @Override
        public void onSuccess() {

            downloadTask = null;

            //下载成功时将正在下载的前台服务通知关闭
            stopForeground(true);

            //创建一个下载成功的通知
            getNotificationManager().notify(1, getNotification("Download Success", -1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onFailed() {
            downloadTask  = null;

            //下载失败时将前台服务通知关闭,并创建一个下载失败的通知,
            // !!!!!后面几个方法(暂停、取消)的逻辑 与此类似!!!!
            stopForeground(true);
            getNotificationManager().notify(1, getNotification("Download Failed", -1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onPaused() {
            downloadTask  = null;
            Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onCanceled() {
            downloadTask  = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();

        }
    };


    /**
     * 创建DownloadBinder内部类,
     * 把需要放给外部调用的Service服务方法写好,
     * 实例化一个DownloadBinder内部类示例,在onBind()中返回,
     * 这样,
     * 当外部界面与本Service绑定,
     * 就可以在 ServiceConnection实例 的 onServiceConnected 回调方法中,
     * 获得这个 具备了 各种准备好的业务方法的 DownloadBinder(Binder、IBinder)实例了
     *
     */
    private DownloadBinder mBinder = new DownloadBinder();
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
    //创建DownloadBinder内部类,
    //把需要放给外部调用的Service服务方法写好
    class DownloadBinder extends Binder {

        /**
         * 开启下载任务
         * @param url 要下载的资源地址
         */
        public void startDownload(String url){

            if (downloadTask == null){

                downloadUrl = url;

                //创建DownloadTask实例
                downloadTask = new DownloadTask(listener);

                //传入下载地址,启动下载任务!!!!
                downloadTask.execute(downloadUrl);

                //让这个下载任务服务成为一个前台服务!!!
                // 使用时在Activity处 先 startService(intent);  启动! 本服务DownloadService
                //
                // 然后 绑定本服务 bindService(intent, connection, BIND_AUTO_CREATE);!!!!
                // 再调用本方法 downloadBinder【即这里的mBinder】.startDownload(url);
                // 运行到下面的startForeground()!!
                // 从而使刚刚已经启动(start)的服务变成前台服务!!!!!
                //这样就会在 系统状态栏 中 创建一个持续运行的通知了
                // .
                // 注意这里有个id!!! 后续取消时 可以用!!

                getNotificationManager();// 配置 NotificationManager!!!!!!!!
                startForeground(1, getNotification("Downloading...", 0));
                //!!!!!!!!!!!

                Toast.makeText(DownloadService.this, "Downloading...", Toast.LENGTH_SHORT).show();
            }
        }
        public void pauseDownload(){
            if (downloadTask != null){
                //使下载任务downloadTask 的 暂停位 置位
                downloadTask.pauseDownload();
            }
        }
        public void cancelDownload(){
            if (downloadTask != null){

                //首先,使下载任务downloadTask 的 取消位 置位,终止下载!!!!
                downloadTask.cancelDownload();
                //调用流程:
                // downloadTask.cancelDownload();
                // --> isCanceled = true;   取消位 置位
                // .
                // -->downloadTask 的 doInBackground 中 取消位 置位生效
                // doInBackground() 中的 下载文件的while循环中
                // if (isCanceled){ return TYPE_CANCELED;} 返回取消位 并终止下载!!!
                // .
                // -->onPostExecute() 接收到 doInBackground()返回的取消位
                // (只要onPostExecute() 接收到了取消位, 便已经终止下载了!! 这时候回调接口...)
                // .
                // --> listener.onCanceled(); 回调 接口的 取消方法 ,
                // 即这里 DownloadService 实现的方法, 接着进行下一步操作...
                // .
                // --> downloadTask  = null;

            }else {
                //如果 downloadTask  = null; 则 执行到此

                //纵观 接口处几个方法 无论成功、失败、暂停、取消
                // 都会执行 downloadTask  = null;
                // .
                // 也就是说 只要 downloadTask 调用过 一次 接口方法!!!!
                // 之后再调用  downloadBinder.cancelDownload(); 的话,
                // 都会已 downloadTask  = null;
                // 即 会执行至此, 删除文件,关闭通知 !!!

                if (downloadUrl != null){

                    //取消下载时需将文件删除,并将通知关闭

                    //获取file 的过程 同DownloadTask 的 doInBackground()
                    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//得到文件名
                    String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directroy + fileName);
                    if (file.exists()){
                        file.delete();
                    }

                    //取消对应id 前台通知或者服务
                    getNotificationManager().cancel(1);
                    stopForeground(true);

                    Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }


    //封装 NotificationManager
    private NotificationManager getNotificationManager(){
        NotificationManager notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
            notificationManager.createNotificationChannel(channel);

            return notificationManager;
        } else {
            return notificationManager;
        }

    }

    /**
     * 封装进度条通知
     * 返回一个封装配置好的 Notification
     *
     * Notification
     * 遇 startForeground() 则成前台服务!!!
     * 遇 NotificationManager.notify() 则成通知!!!
     */
    private Notification getNotification(String title, int progress){

        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);

        //拿着Notification 的 建造者Builder, 去各种配置(set()),
        // 配置完毕了,调用builder.build(),返回 一个 Notification !!!
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationId);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setContentIntent(pi);
        builder.setContentTitle(title);

//        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            builder.setChannelId(notificationId);
//        }

        if (progress > 0){
            //当progress大于或等于0时才需显示下载进度
            builder.setContentText(progress + "%");
            builder.setProgress(100, progress, false);//三个参数:通知的最大进度,通知的当前进度,是否使用模糊进度条
        }

        return builder.build();
    }
}

. 普及

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/start_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Download"/>

    <Button
        android:id="@+id/pause_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pause Download"/>

    <Button
        android:id="@+id/cancel_download"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Cancel Download"/>

</LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private DownloadService.DownloadBinder downloadBinder;

    //创建了一个ServiceConnection 的 匿名内部类,
    // 重写方法后 赋值给ServiceConnection 实例
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();
    }

    private void initViews() {
        //初始化 UI 按钮
        Button startDownload = (Button) findViewById(R.id.start_download);
        Button pauseDownload = (Button) findViewById(R.id.pause_download);
        Button cancelDownload = (Button) findViewById(R.id.cancel_download);
        startDownload.setOnClickListener(this);
        pauseDownload.setOnClickListener(this);
        cancelDownload.setOnClickListener(this);

        //启动服务 以及 绑定服务 二者在这里 缺一不可
        Intent intent =new Intent(this, DownloadService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(intent);//启动服务,保证服务一直在后台运行!!!
        } else {
            startService(intent);
        }
        bindService(intent, connection, BIND_AUTO_CREATE);//绑定服务,让MainActivity和服务进行通信!!!

        //运行时权限申请
        if (ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){

            ActivityCompat.requestPermissions(MainActivity.this, new String[]{
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,}, 1);
        }
    }

    @Override
    public void onClick(View v) {
        if (downloadBinder == null){
            return;
        }
        switch (v.getId()){
            case R.id.start_download:
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;

            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;

            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;

            default:
                break;
        }

    }

    //运行时权限申请结果
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:

                for (int grantResult : grantResults) {
                    if (grantResult != PackageManager.PERMISSION_GRANTED) {
                        Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
                        finish();
                    }
                }

                break;
            default:
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);//解绑服务,避免内存泄漏
    }
}
  • 声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

注意Android 8.0 之后,开启前台服务需要关注一下几点!!!

  • 开启服务需要用startForegroundService(intent), 不能用startService(intent); 且调用完startForegroundService(intent)之后, 五秒内需要调用startForeground()!!! 否则app可能会ANR! 实战如上, MainActivityinitViews()里边的startForegroundService(intent)
  • 需要为 NotificationManager 配置 NotificationChannelDownloadService里面的getNotificationManager()
  • 需要为 Notification 设置 channelIdDownloadService里面的的getNotification()
  • 需要静态声明权限 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

参考文章:

运行测试:

  • 首先是请求权限:
  • Toast提示
  • 开始+暂停+开始(断点续传)
  • 开始+暂停+取消(重新下载)
  • 下载完毕提示
  • 下载完毕点击再开始,不会再下载
  • 下载完毕点击取消会删除文件,再点击开始会重新下载

参考自《第一行代码》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用 pdf.js 跨域问题的处理方法1

    在《使用 pdf.js 在网页中加载 pdf 文件》中详细介绍了 pdf.js 的使用与集成网页开发的基本方法。展示效果如下图:

    张传宁老师
  • 共享可变状态中出现的问题以及如何避免[每日前端夜话0xDB]

    这里有两个独立的部分:函数logElements()和函数main()。后者想要在对数组进行排序的前后都打印其内容。但是它到用了 logElements() ,...

    疯狂的技术宅
  • Java和SQL取两个字符间的值

    String str = "abcdefg";String result = str.substring(str.indexOf(">") 1, str.las...

    chenchenchen
  • 7个常见的 JavaScript 测验及解答[每日前端夜话0xDE]

    我相信学习新事物并评估我们所知的东西对自己的进步非常有用,可以避免了我们觉得自己的知识过时的情况。在本文中,我将介绍一些常见的 JavaScript 知识。请享...

    疯狂的技术宅
  • 在 Node.js 上运行 Flutter Web 应用和 API[每日前端夜话0xDC]

    大量的跨平台应用开发框架,使你可以编写一次代码,然后在 Android,iOS 等多个平台上甚至在台式机上运行。你可能听说过一些流行的框架,例如 Ionic,X...

    疯狂的技术宅
  • C# 8 - using声明 和 异步流

    尽管.NET Core运行时有垃圾收集器(GC)来负责内存清理工作,但是我们还是要自己确保当非托管资源不再使用的时候应该被清理掉。以前针对实现了IDisposa...

    solenovex
  • 设计模式——原型模式

    设计模式中,单例模式应该是大家最为熟悉的了,那如果我们需要对一个对象进行多次复制的话,大家会用什么呢?这就要用到今天要讲的原型模式了。

    健程之道
  • 在现代 JavaScript 中编写异步任务[每日前端夜话0xDD]

    在本文中,我们将探讨过去异步执行的 JavaScript 的演变,以及它是怎样改变我们编写代码的方式的。我们将从最早的 Web 开发开始,一直到现代异步模式。

    疯狂的技术宅
  • 前端接口模拟工具Mock.js上手实践

    在前后端开发过程中,常常会遇到前后端进度不一致的情况,如果前端停下来等后端接口调试完再继续开发会大大降低前端的开发效率。Mock.js 是一款成熟强大的接口模拟...

    CS逍遥剑仙
  • 简单三步快速学会使用Mybatis-Generator自动生成entity实体、dao接口以及mapper映射文件(postgre使用实例)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    eguid

扫码关注云+社区

领取腾讯云代金券