webpack 高级

高级配置就是要进行 webpack 优化,让代码在编译、运行时性能更好

主要从以下角度去优化:

1、提升开发体验

2、提升打包构建速度

3、减少代码体积

4、优化代码运行性能

一、提升体验

1、SourceMap

为什么

打包出来的所有css和js合并成了一个文件,将代码进行了编译、压缩,多了其他代码。这种情况下,进行 debug 是比较困难的,因为看到的代码不仅是一坨,有的较原代码,还做了编译(如剪头函数转换为了普通函数)。

是什么

SourceMap(源代码映射)是一个用来生成源代码和构建代码一一映射的文件的方案。

SourceMap 会在dist目录生成一个 XX.map 文件,里面包含源代码和构建后的代码没一行、每一列的映射关系。当代码报错时,会通过 XXX.map 文件,从构建后代码出错的位置,找到映射后源代码的出错位置,从而让浏览器提示源代码文件出错的位置,帮助我们更快的找到错误根源。

怎么用

js 复制代码
//webpack.dev.js 开发模式下
//优点:打包编译速度快,只包含行映射
//缺点:没有列映射
// 其他省略
module.exports = {
	mode: "development",
	devtool: "cheap-module-source-map",
};

//webpack.prod.js 生产模式下
//优点:包含行/列映射
//缺点:打包编译速度更慢
module.exports = {
  // 其他省略
  mode: "production",
  devtool: "source-map",
};

说明

1、使用SourceMap会在打包输出目录的对应位置,生产 XX.map 文件

2、css、js 都会生产 .map 文件

3、使用 SourceMap 后,在浏览器控制台的 Sources 功能区,可以通过 Command+p 搜索对应的文件 (如main.js、main.css)

二、提升打包构建速度

1、HotModuleReplacement(HMR/热模块替换)

是什么

HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,无需重新加载整个页面(刷新页面)。

只需要在开发环境上配置,生产环境不需要。

怎么用

css

devServer.hot 在webpack5时时默认开启的,

js 复制代码
module.exports = {
  //开发服务器:不会输出资源,在内存中编译打包
  devServer: {
    host: "127.0.0.1",//启动服务器域名
    open: true, //启动服务器端口号
    port: 8000,  //是否自动打开浏览器
    hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
  },
};
js

HotModuleReplacement 只适用与css,js修改还是会重新加载整个页面。要使js实现该功能,需要在 main.js 增加:

js 复制代码
// main.js
// 判断是否支持HMR功能
if (module.hot) {
  module.hot.accept("./js/count.js", function (count) {
  	//js更新后的回调
    const result1 = count(2, 1);
    console.log(result1);
  });

  module.hot.accept("./js/sum.js", function (sum) {
  	//js更新后的回调
    const result2 = sum(1, 2, 3, 4);
    console.log(result2);
  });
}

以上修改比较麻烦,实际开发中,也可以使用其他 loader 来解决(如:vue-loader,react-hot-loader)。

2、OneOf

为什么

打包时,每个文件都会经过所有的loader 处理,虽然因为test正在原因实际上没有处理,但是都要比对一遍,比较慢。

是什么

只要匹配上一个 loader,剩下的就不进行匹配。

怎么用

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        oneOf: [
        	 {
		        test: /\.css$/i,  //使用正则匹配.css后缀的文件
		        //执行顺序:从右到左(从上到下)
		        use:[
		          "style-loader", //将js中的css通过创建style标签的方式,添加到html中
		          "css-loader"  //将css资源编译成commonjs的模块到js中
		        ]
		      },
		      {
		        test: /\.less$/,
		        use: [
		          "style-loader",
		          "css-loader",
		          "less-loader" //将less编译成css文件
		        ]
		    },
        ]
      }
    ]
  }
}

3、Include/Exclude

为什么

开发时的第三方库或插件,不需要再进行编译,可以直接使用,所以在对js文件处理时,要做排除。

是什么

  • include
    包含,只处理XXX文件
  • exclude
    排除,除了XX文件外,其他文件都处理

怎么用

可以使用在loader里面,也可使用在 plugin 中,使用方法是一样的

js 复制代码
//webpack.X.js
//loader修改
{
   test: /\.js$/,
   // exclude: /node_modules/, // 排除node_modules代码不编译
   include: path.resolve(__dirname, "../src"), // 也可以用包含
   loader: 'babel-loader',
   // options: {
   //   presets: ['@babel/preset-env']
   // }
 }

4、cache

为什么

每次打包时,js文件都要经过Eslint检查和Babel编译,速度比较慢。

可以缓存之前的Eslint检查和Babel编译结果,这样第二次打包的速度就是更快了。

是什么

对Eslint检查和Babel编译结果进行缓存,缓存目录在 node_modules/.cache 下

怎么用

js 复制代码
//webpack.XXX.js,开发环境、生产环境都可以使用
//module.rules 下的loader
{
   test: /\.js$/,
    // exclude: /node_modules/, // 排除node_modules代码不编译
    include: path.resolve(__dirname, "../src"), // 也可以用包含
    loader: 'babel-loader',
    options: {
      // presets: ['@babel/preset-env'],
      cacheDirectory: true,//开启Babel编译缓存
      cacheCompression: false,//缓存文件不要压缩(这里缓存是缓存在本地的)
    }
  }
js 复制代码
plugins: [
  new ESLintWebpackPlugin({
    // 指定检查文件的根目录
    context: path.resolve(__dirname, "../src"),
    exclude: "node_modules", // 默认值
    cache: true, // 开启缓存
    // 缓存目录
    cacheLocation: path.resolve(
      __dirname,
      "../node_modules/.cache/.eslintcache"
    ),
  })
],

5、Thead 多进程

为什么

当项目越来约庞大,打包速度会越来越慢。想要提升打包速度,其实就是提升js的打包速度,因为其他文件比较少。

对js文件处理主要是 eslint、babel、Terser 三个工具,所以要提升他们的运行速度。

可以开启多个进程同事处理js文件。

是什么

多进程打包:开启电脑的多个进程同事干一件事,速度较快。

注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大概600ms左右的开销。

怎么用

启动经常的数据就是cup的核数

1、下载包

js 复制代码
$ npm i thread-loader -D

2、获取cpu的核数(每个电脑都不一样)

js 复制代码
// nodejs核心模块,直接使用
const os = require("os")
//cpu核数
const threads = os.cpus().length

3、js 使用

js 复制代码
//webpack.xxx.js 中,js-loader修改如下:
{
   test: /\.js$/,
    // exclude: /node_modules/, // 排除node_modules代码不编译
    include: path.resolve(__dirname, "../src"), // 也可以用包含
    use: [
      {
        loader: "thread-loader",
        options: {
          workers: threads,
        }
      },
      {
        loader: 'babel-loader',
        options: {
          // presets: ['@babel/preset-env'],
          cacheDirectory: true,//开启Babel编译缓存
          cacheCompression: false,//缓存文件不要压缩(这里缓存是缓存在本地的)
        }
      }
    ]
  }

4、css 使用

js 复制代码
//webpack.xxx.js 中,js-loader修改如下:

const TerserPlugin = require("terser-webpack-plugin")

//css增加进程可以写到 plugin 下面,也可以写到 optimization 下面,效果一样,webpack5推荐写到 optimization 中
plugins: [
  //plugin的配置
  //将css输出到指定目录
  // new CssMinimizerPlugin(), //压缩css文件
  // new TerserPlugin({
  //   parallel: threads
  // })
],
optimization: {
    minimize: true,
    minimizer: [
      //css压缩不仅可以写到plugin中,也可写到optimization。minimizer中,官方推荐就是这里,效果一样
      new CssMinimizerPlugin(), //压缩css文件
      //生产模式会默认开启TerserPlugin,这里需要进行其他配置,就需要重新写
      new TerserPlugin({
        parallel: threads, //开启多进程
      })
    ]
  },

三、减少代码体积

1、Tree Shaking

为什么

开发时定义的一些工具函数,或者饮用的第三方工具函数或组件库,如果没有特殊处理,打包时会引入整个库,但是实际上我们可能只用了极小部分的功能,这样整个库都打包进来,体积会很大。

是什么

Tree Shaking是一个属术语,通常用与描述一处 Javascript 中没有使用上的代码。

注:它依赖 ES Module

怎么用

Webpack 已经默认开启了这个功能,无需其他配置。

2、Babel

为什么

Babel 为编译的每个文件都插入了辅助的代码,是代码体积过大。

Babel 对一些公共方法使用了非常小的辅助代码,比如_extend。默认情况下,会被添加到每一个需要它的文件中。

是什么

@babel/plugin-transform-runtime:禁用了 Babel 自动对每个文件的 runtime 注入,而是饮用@babel/plugin-transform-runtime并且使所有辅助代码从这里引用。

怎么用

js loader中,增加 plugins: ["@babel/plugin-transform-runtime"]

js 复制代码
//webpack.prod.js
{
  test: /\.js$/,
  // exclude: /node_modules/, // 排除node_modules代码不编译
  include: path.resolve(__dirname, "../src"), // 也可以用包含
  use: [
    {
      loader: "thread-loader",
      options: {
        workers: threads,
      }
    },
    {
      loader: 'babel-loader',
      options: {
        // presets: ['@babel/preset-env'],
        cacheDirectory: true,//开启Babel编译缓存
        cacheCompression: false,//缓存文件不要压缩(这里缓存是缓存在本地的)
        plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
      }
    }
  ]
}

3、Image Minimizer

为什么

如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。

开发可以对图片进行压缩,减少图片体积。

注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。

是什么

image-minimizer-webpack-plugin:用来压缩图片的插件

怎么用

1、下载包

js 复制代码
$ npm i image-minimizer-webpack-plugin imagemin -D

还有剩下的包需要下载,分两种模式

  • 无损压缩
js 复制代码
$ npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
  • 有损压缩
js 复制代码
$ npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

2、配置

js 复制代码
//引入
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
//------------------------

optimization: {
  minimizer: [
    // 压缩图片
    new ImageMinimizerPlugin({
      minimizer: {
        implementation: ImageMinimizerPlugin.imageminGenerate,
        options: {
          plugins: [
            ["gifsicle", { interlaced: true }],
            ["jpegtran", { progressive: true }],
            ["optipng", { optimizationLevel: 5 }],
            [
              "svgo",
              {
                plugins: [
                  "preset-default",
                  "prefixIds",
                  {
                    name: "sortAttrs",
                    params: {
                      xmlnsOrder: "alphabetical",
                    },
                  },
                ],
              },
            ],
          ],
        },
      },
    }),
  ],

四、优化代码运行性能

1、Code Split

为什么

打包代码时,会将所有的 js 文件打包到一个文件中,体积太大了。如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。

所以需要将打包生产的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源少了,速度就更快。

是什么

代码分割(Code Split)主要做了两件事:

1、分割文件:将打包生产的文件进行分割,生产多个 js 文件

2、按需加载:需要哪个文件就加载哪个文件

怎么用

1、创建一个新的目录,目录结构如下(其中 src 是多入口文件):

2、初始化webpack项目,下载包

js 复制代码
$ npm init -y   
$ npm i webpack webpack-cli html-webpack-plugin -D

3、安装目录结构新建文件,src中的随便写点东西,主要是 webpack.cofig.js 的配置

js 复制代码
//webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  // enrty: "./src/main.js",  //只有一个入口文件,单入口时使用这种方式
  entry: {
    //有多个入口文件,多入口时, entry 要使用对象,
    //对象包含:输出的文件名:入口文件路径
    app: "./src/app.js",
    main: "./src/main.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].js" //webpack命名方式,【name】是enrty中的键名
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html")
    })
  ],
  mode: "production"
}

4、运行

js 复制代码
$ npx webpack

2、Code Split 提取重复代码

如果多入口文件都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。

我们需要提取多入口文件的重复代码,只打包生产一个 js 文件,其他文件引用它就好。

js 复制代码
//webpack.config.js
optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // minSize: 20000, // 分割代码最小的大小
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      //     reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
      // 修改配置
      cacheGroups: {
        // 组,哪些模块要打包到一个组
        // defaultVendors: { // 组名
        //   test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
        //   priority: -10, // 权重(越大越高)
        //   reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
        // },
        default: {
          // 其他没有写的配置会使用上面的默认值
          minSize: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },

3、Code Split 按需加载,动态倒入

场景:页面上有一个按钮,点击按钮的时候加载执行的方法。

1、修改文件

  • main.js
js 复制代码
import {sum} from "./math"
// import count from "./count" //普通方式引入方法
console.log('main');
console.log(sum(5,6));

document.getElementById("btn").onclick = function() {
  // console.log(count(2,3));
  import('./count.js').then(({count}) => {
    console.log("按需加载了" + count(1,2,3,4,5));
  })
}
  • public/index.html
js 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>webpack-more-entry</title>
</head>
<body>
  哈哈哈哈
  <button id="btn">点击按钮</button>
</body>
</html>

第 四 部分功能代码见:链接: https://pan.baidu.com/s/15yzdHnrWIKdo5ixeRdnJcw?pwd=hqtf 提取码: hqtf

4、Code Split 单入口

开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要这样配置(主要是optimization.splitChunks.chunks 配置项):

js 复制代码
//webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  entry: "./src/main.js",  //只有一个入口文件,单入口时使用这种方式
  // entry: {
  //   //有多个入口文件,多入口时, entry 要使用对象,
  //   //对象包含:输出的文件名:入口文件路径
  //   app: "./src/app.js",
  //   main: "./src/main.js",
  // },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].js", //webpack命名方式,【name】是enrty中的键名
    clean: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "./public/index.html")
    })
  ],
  mode: "production",
  optimization: {
    // 代码分割配置
    splitChunks: {
     // 对所有模块都进行分割
     //(把 node-modules 相关代码打包成一个文件;
     //把动态倒入配置打包成一个文件)
      chunks: "all", 
    },
  }
}

5、给模块命名

之前打包出来的文件名都是webpack自动生成的字符串,读起来不太友好。

  • 动态引入时,使用注释语句/* webpackChunkName: "count" */'把名字写进去
    方法:
js 复制代码
// main.js
import {sum} from "./math"
// import count from "./count" //普通方式引入方法
console.log('main');
console.log(sum(5,6));

document.getElementById("btn").onclick = function() {
  // console.log(count(2,3));
  // /* webpackChunkName: "count" */' webpack魔法命名,以注释的方式写入
  import(/* webpackChunkName: "count" */'./count.js').then(({count}) => {
    console.log("按需加载了" + count(1,2,3,4,5));
  })
}
  • 修改webpack的输出配置(chunkFilename)
js 复制代码
{
  path: path.resolve(__dirname, "dist"),
  filename: "js/[name].js", //webpack命名方式,【name】是enrty中的键名
  chunkFilename: "js/[name].js",  //打包输出的其他文件名
  clean: true
},

6、统一命名配置

  • 文件输出配置项,配置chunk名字以及图片字体等文件资源的名字,统一格式的名字
js 复制代码
output: {
    path: path.resolve(__dirname, "dist"),
    filename: "js/[name].js", //webpack命名方式,【name】是enrty中的键名
    chunkFilename: "js/[name].chunk.js",  //动态输出资源命名方式
    assetModuleFilename: "media/[name].[hash][ext]",//图片、字体等资源命名方式
    clean: true
  },
  • css、图片等相关资源的loader中,文件名配置项 generator 去掉
    打包出来的文件如下

2、Preload/Prefetch

为什么

前面已经做了代码分割,同时会使用 import 动态倒入语法来进行代码按需加载(也叫懒加载,比如路由懒加载就是这样实现的)。

但是加载速度还不够好,如:是用户点击按钮才加载这个资源的,如果资源体积很大,那么用户会感到明显的卡顿效果。
PreloadPrefetch技术,是在浏览器空闲的时间,加载后续需要使用的资源。

是什么

  • Preload是告诉浏览器立即加载资源
  • Prefetch是告诉浏览器在空闲时,才开始加载资源

共同点:

  • 都只会加载资源,并不执行
  • 都有缓存

区别:

  • Preload加载优先级高,Prefetch加载优先级低
  • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源

总结:

  • 当前页面优先级高的资源用Preload加载
  • 下一个页面页面需要使用的资源用Prefetch加载

问题:兼容性较差,Preload相对于Prefetch兼容性好一点

怎么用

  • 下载包
js 复制代码
$ npm i @vue/preload-webpack-plugin -D
  • webpack.config.js 中配置
js 复制代码
plugins: [
  new PreloadWebpackPlugin({
    // rel: "preload",
    // as: "script"
    rel: "prefetch"
  })
],

3、Core-js

为什么

在这之前使用的 babel 对 js 代码进行了兼容性处理,其中,使用 @babel/preset-env 智能预设来处理兼容性问题,它能将 ES5 的一些语法进行编译转换,比如剪头函数、三点运算符等。但是如果是 async 函数,promise 对象,数组的一些方法(includes)等,它无法处理。

所以,此时 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以需要将 js 兼容性问题彻底解决。

是什么

core-js是专门用来做 ES6 以及以上 API 的polyfill
polyfill翻译过来叫垫片、补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性等浏览器上,使用该新特性

怎么用

  • main.js
js 复制代码
//1、引入core-js下的所以资源,缺点:打出的包会比较大
// import "core-js"
//2、按需引入,缺点:手动引入,容易遗漏
// import "core-js/es/promise"
//3、自动按需引入,方法:仅需配置 babel.config.js

new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
    console.log("promise");
  }, 1000);
})
const arr = [1,2,3,4,5]
console.log(arr.includes(1));
  • babel.config.js
js 复制代码
module.exports = {
  // 智能预设:能够编译ES6语法
  presets: [
    [
      "@babel/preset-env",
      // 按需加载core-js的polyfill
      { 
        useBuiltIns: "usage", 
        corejs: { version: "3.32.2", proposals: true } },
    ],
  ],
};

4、PWA

为什么

开发 Web App 项目,项目一旦处理网络离线情况,就没发访问了。我们希望给项目提供离线体验。

是什么

渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序)体验的 Web App 的技术。

其中最重要的是,在离现象(offline)时,应用程序能够继续运行功能。

内部是通过 Service Woekers 技术实现的。

怎么用

  • 下载包
js 复制代码
$ npm i workbox-webpack-plugin -D
  • webpack.config.js 中增加:
js 复制代码
//引入workbox-webpack-plugin
const WorkboxPlugin = require("workbox-webpack-plugin");
//plugin中使用WorkboxPlugin
plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, "./public/index.html")
  }),
  new PreloadWebpackPlugin({
    // rel: "preload",
    // as: "script"
    rel: "prefetch"
  }),
  new WorkboxPlugin.GenerateSW({
    // 这些选项帮助快速启用 ServiceWorkers
    // 不允许遗留任何"旧的" ServiceWorkers
    clientsClaim: true,
    skipWaiting: true,
  }),
],
  • main.js 增加判断
    因为 serviceWorker 具有兼容性问题,这里要加一下判断
js 复制代码
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker
      .register("/service-worker.js")
      .then((registration) => {
        console.log("SW registered: ", registration);
      })
      .catch((registrationError) => {
        console.log("SW registration failed: ", registrationError);
      });
  });
}
  • 重新打包,打包出来的目录下多了service-worker.js和workbox-XXX.js

    此时如果直接在浏览器中打开 dist/index.html,将浏览器置为 offline,控制台是会报错的(找不到相关服务),这时需要下载 serve
js 复制代码
$ npm i serve -g

之后执行启动命令 serve dist,根据输出ip打开即可,此时设置 offline 时,刷新页面,页面内容还是存在的。

总结

以上代码参考:链接: https://pan.baidu.com/s/1Y9fcso1l-Cyt-eIGKSfqow?pwd=9jh0 提取码: 9jh0

相关推荐
热爱编程的小曾18 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin29 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox42 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758102 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox