Giter Club home page Giter Club logo

blog's People

Contributors

psycholali avatar

Watchers

 avatar

blog's Issues

小白解密安卓机上微信聊天记录

安卓小白,出于调研“如何迁移微信聊天记录”而去破解了一下安卓机的微信聊天记录数据库文件,这个破解方式2013年就已经有人发表了文章,有兴趣可以去网上搜一搜,这篇文章主要是记录学习历程以及遇到的问题。

##具体过程##
1.聊天记录信息全保存在手机**/data/data目录里。这个目录需要root**之后,手机上才能看到此目录(但是电脑端还是不可直接查看)

2.通过adb连接手机,然后adb shell使用su权限授权/data/data 777权限,然后就可以通过电脑上的android studio里的android device monitor查看此目录结构

3.android device monitor可以选择文件并导出到电脑上,进行进一步查看
image

4.微信聊天记录位置
/data/data/com.tencent.mm/MicroMsg/一串MD5 32位小写格式的文件夹/EnMicroMsg.db里
(这串MD5 32位小写格式的文件夹名是通过mm+uin然后MD5加密而成的)

5.这个EnMicroMsg.db有密码,密码是手机imei+uin 然后进行MD5的32位小写加密,并截取前7位

  • 获取 imei号: 在手机的打电话拨号盘输入*#6#获取
  • 获取 uin:
    方法一:访问/data/data/com.tencent.mm/MicroMsg/*/system_config_prefs.xml,获取其中name="default_uin" value="([0-9]+)"的value字段值uin。(我用的这个,有效)
    方法二:也可以打开wx.qq.com网页版,查找.wx.qq.com域的cookie,其中wxuin字段的值就是uin。(试过,密码错误,好像和正负数有关)
    方法三:也可以用backup.tar里的apps/com.tencent.mm/sp/system_config_prefs.xml。

然后网上大部分说这个加密算法是不会变化的(MD5(IMEI+UIN).Substring(0, 7).toLower),因为维系为了兼容以前版本,如果数据库加密算法变动,那么老版本的用户升级到新版本,老版本的数据库解密就会失败的,也就是用户看不到以前的消息内容,那不可能的

6.一般数据库软件像navicat无法打开EnMicroMsg.db,需要专门的处理加解密的数据库软件打开,我用的大部分人推荐的sqlcipher。(这是一个开源的库,听说如果版本不同,使用正确的密码也不能打开,现在使用的是v2.1版本,可以打开

7.成功打开聊天记录信息的数据库,可以看到表结构
image

8.可以去到Browse Date顶部菜单里查看数据数据:

用户基本信息——userinfo表
联系人——rcontact表
聊天记录——message表
已发视频信息——videoinfo2表
已发图片信息——imginfo2表
已发语音信息——voiceinfo2表

然后如果手机微信选择某一条信息删除,然后db就会更新为删除后的表。

参考资料:
https://www.osslab.com.tw/how-decrypt-wechat-sqlite/
https://articles.forensicfocus.com/2014/10/01/decrypt-wechat-enmicromsgdb-database/
https://blog.csdn.net/njweiyukun/article/details/54024442

【译】首次有效渲染时间(Time to First Meaningful Paint)

首次有效渲染时间(Time to First Meaningful Paint)

一个基于布局的近似方法(A layout-based approach)

[Public] Created: April 12, 2016
Last updated: June 6, 2016 Owner: Kunihiko Sakamoto <[[email protected]][1]>
Status: draft Tracking bug
原文地址:[https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/view#heading=h.p4gz01f31cgq][2]
翻译:psychola(第一次翻译此类文章,有翻译错误请指出哦,如若转载请注明出处哦,谢谢(^U^)ノ~)

背景

**“首次有效渲染”*(First Meaningful Paint)*是当页面的主要内容出现在屏幕上花的时间。这将是我们对于用户感受到的加载体验的主要度量标准(Metric)

“主要内容”*(primary content)*的定义根据不同页面而有所不同。对于博客里的文章,它是标题+首页可见的上半部分文章内容(above-the-fold text),并且其文本必须是可见的,而不是在等待字体加载。对于搜索引擎,它是搜索结果。如果一个图片对于页面来说很重要(例如e-commerce产品页),则 “首次有效渲染”需要这个图片是可见的状态。所以,一个渲染如果只有页面头部、导航栏,或加载提示标(如旋转的图标)就不是合格的“主要内容”。

“首次有内容的渲染”*(First Contentful Paint)*是当一些有内容的东西(如文字、图像、canvas画布或SVG)第一次被渲染花的时间。它在UMA和trace event中需要用到,并且它可以作为“首次有效渲染”时间的近似值,但它经常捕获没有意义的渲染,如头部和导航栏。

在本文中,我们提出一个基于布局的方法去近似求“首次有效渲染时间”,对于用户感知到的“首次有效渲染”时间,这个方法的准确度有77%(总共198个页面)。

设计

基本方法:计算布局对象*(layout objects)*个数

随着页面加载, 布局对象逐步被加到布局树*(layout tree)*中。
图1显示了当加载谷歌搜索结果页面时,被逐步加载到布局树中的布局对象的数量。图2是那个页面加载的视觉进展过程,从WebPageTest结果中可以看到。 (完整的WPT网页测试结果)。

“首次有内容的渲染”(1.577秒时)只有页面头部,并且到那个时候60个“布局对象”被添加到页面。在1.76 s时,顶部广告已渲染了一部分,并且“布局对象”总数是103个。 在大约1.86 s时, 搜索结果出现了, 并且261个“布局对象”被添加到“布局树”中。接下来的渲染(1.907秒时)是“首次有效渲染”。在那之后,剩下的搜索结果和页脚都出现在了下面那些不可见的区域中。视觉上,页面的渲染在2.425秒时完成。

image

图1:当加载谷歌搜索结果页面时,被逐步加载到布局树中的布局对象的数量。

image

图2:页面加载的视觉进展过程

从这个例子中,你可以观察到”布局对象”数量与页面加载的“完整性”密切相关。

(LayoutAnalyzer)通过几个计数器收集有关布局操作的信息,并释放他们作为(trace events)。我用这些计数器做实验并且发现 有大量的 “尚未布局的布局对象”*(LayoutObjectsThatHadNeverHadLayout)的计数器是“首次有效渲染”的一个很好的参考项。其他的计数器, 像“尺寸改变的布局块”( LayoutBlockSizeChanged)或“全都布局好了的布局对象”(TotalLayoutObjectsThatWereLaidOut)都对“首次有效渲染”参考更小。这意味着,按照渲染的重要程度, 新的布局对象的数量比“重新布局的布局对象”的数量(re­layouts)*更有意义。

所以,这是我们第一次求的近似的“首次有效渲染时间”:

“首次有效渲染时间 ”= 紧跟着“最大布局变化”之后的渲染时间点

“最大布局变化”*(Biggest layout change)*就是 有着大量 “尚未布局的布局对象” 的布局。
在图1中, “最大布局变化”是在1.86秒时, 所以下一个渲染时间点(1.907秒)就是“首次有效渲染时间”。

为什么用“布局对象”的总数来定义“首次有效渲染时间”呢?例如,我们能不能定义“首次有效渲染时间”为 百分之多少的布局已经完成呢?问题是我们不能可靠地区分这些东西,当页面完成时。我们不希望根据我们判断一个页面是否加载完成的不同, 使得计算的“首次有效渲染时间”也大幅度的不同。

算法

这一基本方法比“首次有内容的渲染”得到更好的结果 (见后文中的“评估单元”),但在许多情况下它仍然不能检测到真正有意义的渲染。

现在,存在一些这个基本方法不起作用的典型原因, 所以我们要通过添加一些算法,使我们即使在这些情况下也能显著地提高精确度。

1.缓解长页面

图3是http://us.weibo.com/gb页面在视觉上的加载过程([完整的WebPageTest结果](http://www.webpagetest.org/result/160329\_NZ\_GNA/))。
图4显示了在页面加载期间被添加的布局对象的数量。“首次有效渲染时间”是6.047 s(当在一堆文本内容出现时),但最大的布局变化大约是在23.8 s。接下来的渲染(24.25秒时)在视觉上是没有意义的,因为在23.8 s发生的布局变化时添加的对象在页面底部,到了可见区域之外。

image
image
图3: 网站http://us.weibo.com/gb的视觉上的加载过程

image

图4: 页面加载期间被添加的布局对象的数量变化图

我们该如何阻止这些“低于折叠的布局”(below-the-fold layouts)混淆我们的度量标准呢? 对于我们来说,最理想的是去检查每个布局是否是可见的, 但是如果想让这个变成一个UMA度量标准, 就必须避免布局期间所带来的昂贵的计算。所以, 当页面比屏幕高度长的时候,我们降低了页面的布局。具体来说,我们使用“布局意义”来代替“布局对象的原始数量”,如下:

“布局意义”( layout significance )= 添加的布局对象的数量/ max(1,页面高度/屏幕高度)

例如,在图4中最大的布局变化发生的时候(23.8秒),页面高度是屏幕高度的4.25倍, 所以这个“布局意义”是布局对象的数量 除以4.25。图5显示的布局过程与图4类似,但图5使用了上面定义的“布局意义”。现在,最重要的布局变化是在5.89秒, 就在“首次有效渲染”(6.047秒)之前。

image
图 5: 布局意义(布局对象数量由页面高度确定权重)

2.网页字体

图6是http://www.msn.com/页面视觉的加载过程([完整的WebPageTest结果](https://www.webpagetest.org/result/160329\_GA\_GNN/))。最大的布局变化是在2.51秒,但下面的截图没有文本内容,这是因为网络字体仍然加载。

image

图6: http://www.msn.com/页面的视觉加载过程

当一个网页字体在加载时,文本已经布局了(备用字体Fallbackfont的字体度量标准是这样判断的),但文本其实并没有在字体阻塞期间渲染出来(默认情况下, 字体阻塞期间是从加载开始的3秒)。Blink浏览器引擎(Blink是一个由Google主導开发的开源浏览器排版引擎)布局层并不关心文本是不是可见的, 但由于文本对于用户感受到的加载体验是很重要的,所以我们的“首次有效渲染”度量标准应把页面字体的可见性考虑进去。

所以我们引入算法:如果当布局发生时还存在加载中的字体,布局的变化统计将会被推迟,直到该字体显示出来。(如果三秒内加载了就用该字体,否则超时了就用备用字体显示)。

但是,把这条规则应用到所有网站字体显得太勉强了——较小的字体,如图标字体并不会阻塞“首次有效渲染时间”。在我实验的实现中, 超过200个字节的网页字体才会阻塞“首次有效渲染时间”。

附加说明

● 这个测量只是基于新的布局对象,不考虑已布局的布局对象的大小或位置的改变。所以这并不适合“稳定的布局”(layout stabilized)的度量标准。
● 对于一些页面, “首次有效渲染”的关键因素是图片。对于等待中的图片,我们可能需要一些算法, 就像等待加载的字体用的算法一样。
● 由于这个指标是基于布局对象的,所以这是不受“未连接到布局树的DOM元素”的影响 (例如display:none的元素)。然而,一些页面渲染内容在覆盖物之下, (例如,www.flipkart.com的初始页),或在一个透明层之上(例如www.adobe.com)去实现一个fadein的加载效果。在这些情况下,我们的方法发现布局变化对用户来说是看不见的, 而且计时是毫无意义的。
● 页面高度算法使这个度量标准依赖于屏幕的大小。这是很正常的,因为我们的兴趣在是折叠渲染区域之上的部分, 但推导这个度量标准可能有点复杂。

评估

为了评估这个方法, 我使用修补版的ChromeAPK,已经在200个热门网站(基于alexa-top,包括子页面)上跑了WPT测试。然后我下载WPT 的trace.json文件,并且通过运行trace event来计算上文中提到的基于布局的“首次有效渲染时间”。WPT还生成截图幻灯片,所以我们可以比较计算的“首次有效渲染时间”和人为选择的“用户感受到的首次有效渲染时间”的区别。

结果: https://docs.google.com/spreadsheets/d/1FEcDwXfgMf5rw1lNn07Cm04A­r7AdPzmt0aAufMsnnk/ edit?usp=sharing
截屏 (橙色高亮的是检测到的“首次有效渲染时间”): https://goo.gl/wOHl2s

表1总结了结果。“首次有内容的渲染FCP”是我们目前有的最好的近似值,它匹配了52.5%的“用户感知的首次有效渲染时间”[标注1]。而“基本布局对象”(不包含“页面高度算法”或“字体算法”)的准确率有57.1%,略优于“首次有内容的渲染” 。同时包含“页面高度算法”和“网页字体算法”这两个算法的计算准确率高达77.3%。

(标注1)在以前的评估中,我们使用了宽松定义的“首次有效渲染”(屏幕上的部分内容是可读的)。现在我们使用更严格的定义————页面的主要内容必须是易读的。

image

表1:评估结果的总结

实现方案

第 1步: 基于trace的实现

实验的实现包括了:
(1)Blink浏览器引擎改变(Blink side change),这个改变 增强了LayoutAnalyzer对页面高度和网页字体可见性信息的trace event;
(2)一个脚本 ,该脚本处理trace event去计算最大的布局变化。

这个实现方案很容易变成一个TBMv2度量标准*(metrics)*,而这个标准可用于像Page Cycler V2一样的基准。

第2步: UMA

过时了,请看 UMA design doc(UMA设计文档)
我们希望去收集“首次有效渲染时间”作为一个UMA度量标准。Finch和heartbeat依赖于UMA, UMA是唯一我们有的能理解真实用户体验的系统。

LayoutAnalyzer默认情况下是禁用的,因为它有一些计算开销。我们不需要所有的LayoutAnalyzer计数器来计算“首次有效渲染时间”, 只需要计算被添加到布局树的布局对象的数量,这个数量可以以不同的方式收集。

实现计划:PaintTiming持有一个渲染的时间戳,这个时间戳目前在最大的布局变化之后。当有更重要的布局变化时,时间戳会更新。这个时间戳会被发送到浏览器进程的PageLoadMetrics中,并且当用户从页面导航出去时它会作为UMA直方图。

注意, “首次有效渲染时间”的时间戳可以在页面加载期间更改几次。如果页面加载在完成之前失败, “页面加载度量标准”将报告暂时的时间,这个时间可能不是很有意义,例如导航栏或旋转图标。页面加载度量标准” 在单独的直方图里记录“失败的导航栏”的时间 (“页面加载失败时间” *)。对于失败的页面加载,我们需要非常小心地解读它的“首次有效渲染时间”度量标准。

Implementation status

第一步 (基于trace的实现) 来了, 并且它会作为一个TBMv2 metric(TBMv2标准)。现在,你可以从跟踪观察器来看“首次有效渲染时间”。

你可以去chrome://tracing使用此功能, 并勾选下方这两项去记录一个trace:
● blink.user_timing
● loading

“首次有效渲染时间”的值可以在“度量标准”侧板中看到
(见下图)。
image

另外,这些标准可以从一个跟踪文件里计算得到,通过使用
这个命令:

chromium/src$ third_party/catapult/tracing/bin/run_metric trace.json loadingMetric

第二步 (UMA): 第一个版本的UMA来了。目前, 直方图是作为实验的标记内容,在促进他们的实验之前,我们想做一些改进。(跟踪在https://crbug.com/632081).

引用

"Is it useful?" metric (aka. First meaningful paint): layout­based approach loading­dev thread implementation tracking bug

webpack入门必备(二):优化配置

之前写的《webpack入门必备(一):基础配置》主要介绍了webpack基础解析所需的loader/plugin。而随着日常webpack的使用,我们会更多关注如何构建更快、构建产物更小、构建产物符合规范...希望这篇文章可以让你找到答案。

一、webpack4的构建优化

1. 加快构建速度

1.1 优化配置

这里介绍的主要的几种优化配置如下所示:

  1. 缩小构建范围
  • exclude、include范围
  • noParse
  • IgnorePlugin
  1. 多进程
  • thread-loader/happypack
  1. 缓存
  • cache-loader/cacheDirectory,把loader的处理结果缓存到本地
  • Dll缓存,把一些不常变更的模块构建产物缓存在本地

如果你有没用过的配置可以接着看下面的具体使用方法,如果你已经很熟悉了则可以跳过此节~

1. exclude、include范围

配置来确保转译尽可能少的文件(exclude 的优先级高于 include)

const rootDir = process.cwd();

{
        test: /\.(j|t)sx?$/,
        include: [path.resolve(rootDir, 'src')],
        exclude: [
          /(.|_)min\.js$/
        ],
}

PS. 相比exclude可以多用include

2. noParse

如果一些库不依赖其它库的库,不需要解析他们,可以引入来加快编译速度。

noParse: /node_modules\/(moment|chart\.js)/

3. IgnorePlugin

忽略第三方包指定目录。 (他是webpack 内置的插件)

例如: moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容(语言包),见下图。

plugins: [
  // 表示忽略moment下的locale文件夹内容
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

image

4.1 thread-loader

把 thread-loader 放置在其它 loader 之前,那么它之后的 loader 就会在一个单独的 worker 池中运行。

// 项目中babel-loader一般耗时比较长,所以可以配置thread-loader 
rules: [ 
    { 
        test: /\.jsx?$/, 
        use: ['thread-loader', 'cache-loader', 'babel-loader'] 
    } 
] 

4.2 happypack

运行在Node.js上的webpack是单线程,将文件解析的任务拆分由多个子进程并发进行,然后子进程处理完任务后再将结果发送给主进程,提升项目构件速度。
(但是因为进程的分配和管理也需要时间,所以使用后不一定快,需要项目接入实验一下)

const Happypack = require("happypack");
module.exports = {
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: "Happypack/loader?id=js",
        include: [path.resolve(__dirname, "src")],
      },
      {
        test: /\.css$/,
        use: "Happypack/loader?id=css",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
        use: "Happypack/loader?id=file",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "public"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
    ],
  },
  plugins: [
    new Happypack({
      id: "js", //和rule中的id=js对应
      //将之前 rule 中的 loader 在此配置
      use: ["babel-loader"], //必须是数组
    }),
    new Happypack({
      id: "css", //和rule中的id=css对应
      use: ["style-loader", "css-loader", "postcss-loader"],
    }),
    new Happypack({
      id: "file", //和rule中的id=file对应
      use: [
        {
          loader: "url-loader",
          options: {
            limit: 10240, //10K
          },
        },
      ],
    }),
  ],
};

5. cache-loader/cacheDirectory

在性能开销较大的loader处使用,将构建结果缓存中磁盘中。
(默认存在node_modueles/.cache/cache-loader目录下。 )

cacheDirectory例子:

rules: [
      {
            test: /\.(j|t)sx?$/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              }
       }
 ]

cache-loader例子:

rules: [
    {
        test: /\.(css)$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'cache-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' }
        ]
      }
]

6. Dll缓存(动态链接库)

将复用性较高的第三方模块打包到DLL中,再次构建时直接复用,这样只需重新打包业务代码。
(注意是DLL缓存是大大缩短了首次构建时间,像之前的cache-loader优化都是缩短rebuild时间

使用相关插件:

  • DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
  • DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

具体步骤:
(1) 新增一个webpack配置去编译DLL文件([name].dll.js[name].manifest.json

// 新增一个webpack-dll.config.js配置文件

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const distPath = path.resolve(__dirname, 'dll');
module.exports = {
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom'],
    // 把项目需要所有的 polyfill 放到一个单独的动态链接库
    polyfill: [
      'core-js/fn/object/assign',
      'core-js/fn/object/entries',
      ...
    ],
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称(react 和 polyfill)
    filename: '[name].dll.js',
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值(_dll_react)
      name: '_dll_[name]',
      context: process.cwd(),
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
    }),
  ],
};
// package.json里新增dll的构建命令
"scripts": {
    "dll": "webpack --config webpack-dll.config.js",
}

(2) dev构建时,告诉 Webpack 使用了哪些动态链接库

// webpack.config.js文件

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

plugins: [
    // 使用的动态链接库(react和polyfill的)
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'react.manifest.json'),
    }),
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'),
    }),
    ...
]

(3) html template里引入文件

因为我这里只是本地构建加速,所以就以dev的方式引入

<script src="./dll/polyfill.dll.js?_dev"></script>
<script src="./dll/react.dll.js?_dev"></script>

到这DLL就配好了。有些人可能比较好奇react.dll.jsreact.manifast.js到底是什么文件,做了什么事?你看看他两个文件就知道啦~

  • react.dll.js其实主要就是所引用模块的代码集合
  • react.manifast.js则写明包含哪些模块、模块路径
// react.dll.js文件部分内容如下所示。
var _dll_react = (function(modules) {
  // ... 此处省略 webpackBootstrap 函数代码
}([
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 0 的模块对应的代码
  },
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 1 的模块对应的代码
  },
  // ... 此处省略剩下的模块对应的代码 
]));


// react.manifast.js文件部分内容如下所示。
{
  // 描述该动态链接库文件暴露在全局的变量名称
  "name": "_dll_react",
  "content": {
    "./node_modules/process/browser.js": {
      "id": 0,
      "meta": {}
    },
    // ... 此处省略部分模块
    "./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
      "id": 42,
      "meta": {}
    },
     ...
}

1.2 检测工具

常用工具:speed-measure-webpack-plugin
使用方法:用其来包裹 Webpack 的配置

image

2. 构建产物方面

2.1 减小构建产物大小、提高复用率

这里介绍的主要的几种优化配置如下所示:

  • optimization.splitChunks分包
  • babel配置@babel/plugin-transform-runtime
  • tree-shaking

具体使用:

1. optimization.splitChunks分包

将业务代码和第三方依赖库进行分包,减小index.js的大小;
抽离多页应用的公共模块,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。

 optimization: {
    minimize: false,
    moduleIds: 'named',
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 6,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        polyfill: {
          test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
          name: 'polyfill',
          priority: 70,
          minChunks: 1,
          reuseExistingChunk: true
        },
        lib: {
            test: /[\\/]node_modules[\\/]/,
            name: 'lib',
            chunks: 'initial',
            priority: 3,
            minChunks: 1,
         },
       ...
      }
    }
 }

2. babel配置

提取所有页面所需的helper函数到一个包里,避免重复注入

"plugins": [
    "@babel/plugin-transform-runtime"
    ...
]

3. tree-shaking

如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。

(1) 具体配置

const TerserPlugin = require('terser-webpack-plugin');

const config = {
 // 生产模式下tree-shaking才生效
 mode: 'production',
 optimization: {
  // Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
  usedExports: true,
  minimizer: [
   // 删除死代码的压缩器
   new TerserPlugin({...})
  ]
 }
};

(2) 哪类代码会被shake掉?以下有一些事例

// no tree-shaking
import Stuff from './stuff';
doSomething(Stuff);

// tree-shaking
import Stuff from './stuff';
doSomething();

// tree-shaking
import './stuff';
doSomething();

// no tree-shaking
import 'my-lib';
doSomething();

// 全部导入 no tree-shaking
import _ from 'lodash';

// 具名导入 tree-shaking
import { debounce } from 'lodash';

// 直接导入具体的模块  tree-shaking
import debounce from 'lodash/lib/debounce';

(3) 什么叫有副作用的代码?
只要被引入,就会对应用程序产生重要的影响。 (一个很好的例子就是全局样式表,或者设置全局配置的js文件。)

(4) 有副作用的代码我们不希望被shake,我们可以配置如下

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

(5) 注意,babel配置需要配modules: false,忽略import/export代码编译

const config = {
 presets: [
  [
   '@babel/preset-env',
   {
     // commonjs代码不能被tree-shaking
     // 所以babel保留我们现有的 es2015 import/export 语句,不进行编译
    modules: false
   }
  ]
 ]
};

2.2 检测工具

常用工具:webpack-bundle-analyzer
使用方法:用其来包裹 Webpack 的配置

image

3. 产物检查

ES check

生产环境构建时,会检查构建产物里是否存在es6语法。有则抛出错误并提示你去进行babel编译,这样避免了构建产物不合要求的情况。

image
image

具体使用例子:

// package.json 命令里加上es-check检查
"dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"

二、webpack5的构建优化

1. 速度优化

1.1 编译缓存

编译缓存就是在首次编译后把结果缓存起来,在后续编译时复用缓存,从而达到加速编译的效果。
webpack5默认开启编译缓存,缓存默认是在内存里,你可以自定义。

module.exports = {
	cache: {
        // 将缓存类型设置为文件系统
        type: "filesystem", 
        // 缓存的位置(默认是node_modules/.cache/webpack)
        cacheDirectory: path.resolve(__dirname, '.temp_cache'), 
     
        // 指定构建过程中的代码依赖。webpack将使用这些项目以及所有依赖项的哈希值来使文件系统缓存无效。
        buildDependencies: {
     
            // 当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。 
            config: [__filename], 

            // webpack.config、loader和所有从你的配置中require的模块都会被自动添加。如果有其他的东西被构建依赖,你可以在这里添加它们
      },
      
      // 指定缓存的版本。当需要更新配置缓存时,通过设置此版本使缓存失效。
	  version: '1.0'  
    }
}

一些参数注解

  • cache: true 就是 cache: { type: 'memory' } 的别名
  • type: 'filesystem'|'memory'。
    如果设置'memory'则缓存在内存且不能配置其他信息,设置成'filesystem'就可以配置更多信息。默认开发模式使用的是'memory',生产模式是false。
  • version: 当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。

1.2 长效缓存 Long-term caching

长效缓存指的是能充分利用浏览器缓存,尽量减少由于模块变更导致的构建文件hash值的改变,从而导致文件缓存失效。
(由于moduleId和chunkId确定了,构建的文件的hash值也会确定。)

1.2.1 引子

  1. chunk、module都是什么?
  • module:每一个可被导入导出的源码js、css文件就是一个module。
  • chunk:module经webpack依赖分析、打包生成的单独文件块。如:入口entry里的文件、SplitChunks抽离的公共代码
  • bundle:chunk后面经过编译/压缩打包等处理后就变成了bundle,bundle文件直接被html文件引用。

image

  1. webpack提供了以下3种哈希值,分别是什么意思?有啥优缺点?
  • hash 所有bundle文件都是同一个hash。(【缺点】不修改文件的情况下rebuild后hash会更新)
  • chunkhash 同一个entry/及entry引用的chunk文件都是同一个hash。(【缺点】修改chunk文件内容后,这个hash不变)
  • contenthash 一个文件一个hash,修改哪个文件哪个文件的hash就改变。(【缺点】如果删除一个entry里的chunk,entry和chunk及好多个文件的hash都变了,不利于长效缓存。
    比如只是jsx删除引用的一个css文件 好多bundle文件的hash就都变了。)

1.2.2 webpack4实现长效缓存

之前需要通过如下配置达到长效缓存:

plugins: [
- new webpack.NamedModulesPlugin(),
+ new webpack.HashedModuleIdsPlugin(),

或者配置

optimization.moduleIds = 'hashed’ 
optimization.chunkIds = 'named'

配置说明:

  • 在开发环境下使用 NamedModulesPlugin 来固化 module id,在生产环境下使用 HashedModuleIdsPlugin 来固化 module id(因为构建结果文件会更小)
  • 使用 NamedChunksPlugin 来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id
    (NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块(异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/ webpackChunkName: “lodash” / ‘lodash’).then() 这样就有 Name 了),external 模块是不会起作用的。)

1.2.3 Webpack5默认启用deterministic实现长效缓存

Webpack5采用新的算法,生产模式下默认启用如下配置不仅实现长效缓存,还减少了文件打包大小:

optimization.chunkIds: "deterministic"
optimization.moduleIds: "deterministic"
mangleExports: “deterministic"

PS.具体采用的算法还需要进一步深入研究~

2. 包构建大小优化

2.1 Node Polyfill脚本被移除

Webpack 4版本附带了大多数Node.js核心模块的polyfill,一旦前端使用了任何核心模块,这些模块就会自动应用,导致polyfill文件很大,但是其实有些polyfill是不必要的。
而现在webpack5将不会自动为Node.js模块添加Polyfills,需要开发者手动添加合适的Polyfills。

升级迁移至webpack5需要注意:

  • 尽可能尝试使用与前端兼容的模块。
  • 可以为 Node.js 核心模块手动添加 polyfill。错误消息将提示实现方法。
  • 包作者:使用 package.json 中的 browser 字段使包与前端兼容。提供浏览器的替代实现 / 依赖。

2.2 tree-shaking

1.嵌套tree-shaking
能够跟踪对export的嵌套属性的访问,分析模块的export和import的依赖关系,去掉未被使用的模块

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// or import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a); // 在此示例中,可以在生产模式下移除导出 b。

2.内部模块tree-shaking(深度作用域分析)
新属性optimization.innerGraph分析模块导出和导入之间的依赖关系,在生产模式下默认启用。

import { something } from './something';
function usingSomething() {
  return something;
}
export function test() {
  return usingSomething();
}
// 在使用 test 导出时才使用 something。

可以分析以下符号:

  • 函数声明
  • 类声明
  • 带有以下内容的 export default 或变量声明:函数表达式,类表达式,序列表达式,/#PURE/ 表达式,局部变量,导入绑定

3.package.json 中的“sideEffects”标志允许将模块手动标记为无副作用,从而在不使用它们时将其移除。
webpack 5 还可以根据对源代码的静态分析,自动将模块标记为无副作用。

更多Webpack5的内容推荐阅读:

记录微信小程序的坑(2018年记录)

----------------更新--------------

2018年10月10日官网3个接口废弃的通知:

1、分享监听接口
分享消息给好友时,开发者将无法从callback获知用户是否分享完成,也无法在分享后立即获得群ID。请参考调整指引

2、getUserInfo接口
用户需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据。请参考调整指引

3、openSetting接口
用户需要点击行为后,才可以跳转打开设置页,管理授权信息。请参考调整指引

1.不同版本号是否可以使用某方法/属性

官方文档上提到了几种检测方法,但是测试后发现有些没有用
1)**wx.canIUse()**对一些api没有用
2)if (wx.apiXX) { wx.apiXX(); }也不完全有用
所以,最后通过直接比较当前版本号和目标版本号来实现兼容。

/**
 * 比较两个版本号的大小,用于兼容小程序不同版本的api时比较版本号(v1 > v2则返回1)
 * @param {*} v1
 * @param {*} v2
 */
export function compareVersion(v1, v2) {
  v1 = v1.split('.')
  v2 = v2.split('.')
  var len = Math.max(v1.length, v2.length)

  while (v1.length < len) {
    v1.push('0')
  }
  while (v2.length < len) {
    v2.push('0')
  }

  for (var i = 0; i < len; i++) {
    var num1 = parseInt(v1[i])
    var num2 = parseInt(v2[i])

    if (num1 > num2) {
      return 1
    } else if (num1 < num2) {
      return -1
    }
  }

  return 0
}

const  SDKVersion  =  wx.getSystemInfoSync().SDKVersion; // 获取版本
if(T.compareVersion(SDKVersion, '2.0.7')>=0) { ... }

2.小程序间的跳转

小程序跳转只能实现 同一公众号下关联 的小程序跳转
一个公众号可关联10个同主体的小程序,3个不同主体的小程序。
一个小程序可关联3个公众号。
p.s. 所以想跳转像苏宁小程序,只能把我们小程序关联到他们小程序的公众号下)

(1)使用navigator组件(仅用于微信2.0.7以上版本)

<navigator target="miniProgram"  app-id="{{changeNewAppid}}"  path="{{changeNewUrl}}"  version="release"  @tap="goMiniprogram">
跳转mp
</navigator>

(2)使用navigateToMiniProgram来兼容微信2.0.7以下(但即将废弃) >=1.3.0

goMiniprogram() {
	const  SDKVersion  =  wx.getSystemInfoSync().SDKVersion; // 获取版本
	if(T.compareVersion(SDKVersion, '2.0.7')>=0) {
		wx.navigateToMiniProgram({
			appId:  this.changeNewAppid,
			path:  this.changeNewUrl,
			envVersion:  'release',
			success(res) {
				console.log('打开mp成功')
			}
		})
	}
}

调试注意:
开发者工具上不会显示跳转,但我们可以从回调函数里log打印信息,只有真机调试才可以跳转

3.小程序所属的公众号入口及其推送文章等入口进入小程序的url配置

格式:pages/list?source=wxmpcz01 (不要appid,也不要pages前加/

4.保存图片功能(用途之一是分享朋友圈)

思路:
1.小程序不能分享到朋友圈,只能通过保存图片的形式分享。
2.如果保存静态图片,可以直接调接口就行,但是需求是动态的图片(获取用户的头像),所以需要用canvas画图,然后保存成2倍/3倍图

优化的问题:
一、文字有用特殊字体,但是字体文件都比较大。所以需要用fontmin抽取出所需字的字体文件,最后上线页面从原来的3.7MB变成29KB字体woff/eot/ttf文件。

  1. 之前产品想要文案不写死,三种成功页各自有随机的文案,这个在想怎么优化更好。因为写死的文案可以直接抽出来。随机文案要么就是一次性把文案抽出来但字体文件大小会增加;要么就是node写个进程模拟cmd去跑fontmin实时压缩字体的命令,但这种会更慢一些,而且小程序不一定支持这种写法。(一般的PC端网页是支持的)
  2. 小程序中canvas不支持使用自定义字体。所以保存功能需要使用图片替代文字。
  3. 保存图片因为用的canvas,然后canvas每一层无法设置优先级,只能一层层叠上去,所以一些图片/资源异步加载的顺序比较重要,如果加载顺序不做控制则可能将其他内容物覆盖。目前用的是await/aync + Promise.all()来实现
    二、为了使保存的带有二维码的图片更加清晰,保存图片需要使用upng转换成base64格式的。

5.canvas画图

1.canvas的clip()裁剪方法,只对第一次裁剪的图有效,后面裁剪都无效。
解决方案:制作一张和头像图片一样大的中间有个n个圆形镂空(中间透明)的图片绘制在头像上,在视觉上给头像做出圆形的效果。
2.不能给文字设置字体,所以需求需要特殊字体时用的图片。
3.保存canvas为图片时,ctx.draw()需要加个定时器,晚些执行canvasToTempFilePath().否则保存下来的是空白图片。

ctx.draw(true, setTimeout(function() { // 延迟一下
    wx.canvasToTempFilePath({
        x: 0,
        y: 0,
        width: 2079,
        height: 2181,
        destWidth: 2079,
        destHeight: 2181,
        canvasId: 'myCanvas',
        success: function(res) {
            self.data.savedImgUrl = res.tempFilePath;
            self.saveImageToPhoto();
        }
    });
}, 400));
// 保存图片到相册
saveImageToPhoto() {
    const that = this;
    if (this.data.savedImgUrl !== '') {
        wx.saveImageToPhotosAlbum({
            filePath: this.data.savedImgUrl,
            success: function() {
                that.imgSaveLoading = false;
                that.$apply();
                wx.showModal({
                    title: '保存图片成功',
                    content: 'xxxx',
                    showCancel: false
                });
            },
            fail: function(res) {
                console.log(res);
                that.imgSaveLoading = false;
                that.$apply();
                if (res.errMsg === 'saveImageToPhotosAlbum:fail cancel') {
                    wx.showModal({
                        title: '保存图片失败',
                        content: '您已取消保存图片到相册!',
                        showCancel: false
                    });
                } else {
                    wx.showModal({
                        title: '提示',
                        content: '保存图片失败,您可以点击确定设置获取相册权限后再尝试保存!',
                        complete: function(res) {
                            if (res.confirm) {
                                wx.openSetting({});      // 打开小程序设置页面,可以设置权限
                            } else {
                                wx.showModal({
                                    title: '保存图片失败',
                                    content: '您已取消保存图片到相册!',
                                    showCancel: false
                                });
                            }
                        }
                    });
                }
            }
        });
    }
}

参考文章:
小程序05 canvas绘图并保存到相册
canvas坑

6.分享转发功能

如果使用右上角的默认分享功能,可以调用onShareAppMessage(), 如果自定义按钮分享则是使用<button open-type="share"> 结合 onShareAppMessage里加一些判断。(注意:成功/失败callback于2018/10/10废弃)

<button id="share-family" open-type="share">分享给家人</button>

onShareAppMessage(res) {
	let  shareType  =  'friend';
	let  title  =  '送你一个公益礼包,快去打开看看是什么!';
	let  path  =  'XXXX'; // 右上角分享时

	if (res.from  ===  'button') {// 来自页面内转发按钮,根据不同的#id分享不同的链接等操作
		shareType  =  res.target.id.split('-')[1];
		if (shareType  ===  'family') { // 分享结果页
			path  =  ‘XXXXXX2’;
			title  = '我有个公益礼包需要和家人一起打开,你快点进来!'  ;
		}
		....
	}
	return { // 分享的title,path里都可有变量
		title:  title,
		path:  path,
		imageUrl:  'xxx',
		success(res) {
			console.log(res);
			做一些成功后的操作...
		},
		fail(res) {
			console.log(res);
		}
	};
}

7.上报formid给后台实现服务消息触达

1.在微信公众平台-小程序的模板中心先申请一个消息模板
2.服务消息触达只有支付的和提交表单才能触发服务触达通知。而支付这个方法不符合场景,所以使用提交表单:把任意一个文本改造成一个空表单的按钮,然后点击上报formid给后台。(1次提交表单可下发1条,多次提交下发条数独立,相互不影响。)
3.后台使用formid后调用相应触达的接口。

/**
前提条件:
1.已获取access_tocken。
(参考接口:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret})
2.已获取openId。
(参考接口:https://api.weixin.qq.com/sns/jscode2session?appid=${G.appId}&secret=${ appSecret}&js_code=${code}&grant_type=authorization_code)
**/

// 关键代码:
<form bindsubmit="formSubmit" report-submit>
    <button form-type="submit">
	    一个普通文本,把他改造成一个按钮,点击提交空表单来上报formid
    </button>
</form>

formSubmit(e) {
    const { formId } = e.detail; // 获得formid
    ...
},

注意:
1.只能手机调试,我用开发工具打印出来的formId: "the formId is a mock one"并不是数字串。
2.每个formid只能给当前用户推送的时候用 不能给其他人用。

除官方api外的参考文章:
微信小程序实例:创建下发模板消息实例
手把手教你开发微信小程序之模版消息
开发 | 教你突破小程序模板消息的推送限制

8.获取用户信息接口wx.getUserInfo的废弃问题

wx.getUserInfo接口是获取用户信息(昵称,头像等)的接口,在官方文档上写是即将废弃。现在开发版和体验版已经废弃(调用接口默认直接fail),但是现网版本还是可以使用(会出现系统弹弹窗),官网更新说于2018/10/10废弃
目前,有两种兼容方式:
1.如果只是单纯展示用户头像或昵称,可以使用 <open-data > 组件。但是这个有局限性,只能显示,却获取不到信息,比如:后台接口需要前端传递用户昵称或头像信息时。

<open-data type="userAvatarUrl"></open-data>

2.使用**<button open-type="getUserInfo">**,引导用户主动进行授权操作(适配v1.3.0以上版本)

<button open-type="getUserInfo" bindgetuserinfo="onGotUserInfo"\>
onGotUserInfo(e){
  const { errMsg, userInfo } = e.detail;
  if(errMsg.indexOf('ok') != -1) { //同意授权
    info = e.detail.userInfo;
  }
  const { avatarUrl, nickName } = info; // 获取到了头像,昵称等
  ...
},

官方api说明废弃问题:
https://developers.weixin.qq.com/blogdetail?action=get_post_info&lang=zh_CN&token=1650183953&docid=0000a26e1aca6012e896a517556c01

9.生成二维码

参考以下api的三种api可以获取二维码,但是有局限性。比如说:可接受path参数较长,生成个数受限;有的可接受页面参数较短,生成个数不受限;
https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html

1)获取access_token的接口 GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wx8dc21e89439d0e82&secret=78a130feb9f9a44e05fed6839aa4f881

2)获取二维码图片的文件流接口 POST
https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN
{ "path": "pages/list" }

10.<rich-text>组件

因为说明弹窗的文字是运营在工具上配置的一些字段,然后现在有一些关键字高亮的功能,那样运营可能要配置html字符串片段或者其他字符串片段,但小程序不可以像H5直接插入html片段,高亮实现起来比较麻烦
网上解决方法:有的使用wxParse工具库,有的是自己写正则然后去自己拼凑标签
目前解决方法:使用小程序的<rich-text>组件,把内容字段定义成json形式(而不是html格式,免去了手动正则匹配html标签的步骤),然后来动态生成相应标签并动态添加高亮class,但有个不足的地方就是每个text片段如果高亮class是hightlight,而不符合的会有一个空的class。

ps.不采用wxParse的原因:
需要引入7个文件然后总大小134KB,感觉比较占体积。

11.拒绝地理位置授权后重新拉起授权窗口

当用户选择一些个性化偏好设置之后,系统会把选择保存在授权缓存里,后续碰到相同授权不会再系统弹窗询问,而是默认以第一次用户的选择为准(获取地理位置也属于这种情况)。现在需求是希望能点击“获取当前地址”之后再次弹窗询问用户授权。

api bug:
如果用户拒绝第一次系统授权弹窗,后面wx.authorize()拉取授权窗口的接口将无效。(在wx.authorize的success回调里调用后台获取地址接口会直接fail, 报{errMsg: "getLocation:fail auth deny"}的错)

解决方案:
通过一个点击操作来调起openSetting。先去查用户是否授权地理位置(wx.getSetting + authSetting['scope.userLocation']), 如果未授权则打开一个自定义的弹窗询问用户是否去“设置”中打开权限(wx.showModal + wx.openSetting),然后跳转去到“设置”。如果“设置”中用户打开地理权限按钮,就在成功的callback里去调获取地址的API,后续操作就和第一次进入页面一致了。(这里的“设置”是指小程序的授权设置页,非手机设置或者自定义的设置页)

版本兼容:
2.3.0版本开始,用户只有发生点击行为后,才可以跳转打开设置页,所以不能直接调用,如onLoad里就调用。(2018/10/10生效)
1)<button open-type="openSetting" bindopensetting="openSettingFn"> 按钮兼容 (v2.0.7以上生效)
2)<button @tap="openSettingFn">自动获取地址</button>绑定的点击事件openSettingFn还是可以调用api

// 写法1
<button open-type="openSetting"  bindopensetting="openSetting">自动获取地址</button>
openSetting(e) {
  //判断是否获得了用户地理位置授权(v2.0.7以上版本)
  const that = this;
  if(e.detail.authSetting['scope.userLocation']) { 
    //同意用户的地理位置授权 立刻打开“设置”
    const getUserLocationWrapper = function(){
      API.getUserLocation().then((location)=>{
        ...
      }).catch((error) => {
        console.log('发起api 失败',error)
      });
    }
    // 设置一个延时 因为用户打开授权按钮不能立即生效 所以会出现请求接口auth deny的问题
    setTimeout(getUserLocationWrapper, 500);
  }
}

// 写法2
<view class="edit-item-authorize" @tap="dealLocationAuthorize">
  <text>自动获取地址</text>
</view>

dealLocationAuthorize() {
  //判断是否获得了用户地理位置授权(v2.0.7以下版本)
  wx.getSetting({
    success: (res) => {
      if (!res.authSetting['scope.userLocation']){
        this.openConfirm()
      }
    }
  })
}

// 未授权地址时,需要打开自定义的弹窗,询问用户是否去设置中打开权限
openConfirm () {
  const that = this;
  //此处可以打开一个modal询问,然后在success的回调里调用openSetting都行的
  //wx.showModal({
    //content: 'XXXX要获取您的地理位置,是否允许?',
    //confirmText: "允许",
    //cancelText: "不允许",
    //success: function (res) {
      //if (res.confirm) { //点击“确认”时打开设置页面
      
        wx.openSetting({ // openSetting打开“设置”(v2.0.7以下版本)
          success: (res) => {
            const getUserLocationWrapper = function(){
              API.getUserLocation().then((location)=>{
                ...
                console.log('调用后台接口拿到位置信息', location)
              }).catch((error) => {
                console.log('发起调用后台接口失败',error)
              });
            }
            // 设置一个延时 因为用户打开授权按钮不能立即生效 所以会出现请求接口auth deny的问题
            setTimeout(getUserLocationWrapper, 500);
          }
        })
        
      //} else {
       // console.log('用户点击取消')
      //}
      //that.$apply();
    //}
  //});
}

12.有时候开发者模式自测通过,但是发的体验版一些数据拿不到,然后当体验版开了调试模式时,又可以跑通所有逻辑。

解决过程:后来发现是请求方式的问题,dev环境都是http请求,idc则需要https的请求,所以之前只有开了调试模式才能拿到数据。然后,还发现因为调试者工具上开发时默认勾选“不校验合法域名、https证书...”的选项所以开发时未报错,如果取消勾选则会报http那个域名不在白名单内报错(我们小程序白名单用的是https的url)。

还发现了一些奇怪的现象:
像小程序开发板/体验版开了调试模式,再去打开线上小程序,本没有调试工具的线上小程序也有了调试工具。以及这三个版本小程序的缓存感觉有一定联系,可能共用。具体待实验后跟进。

十分钟了解git那些“不常用”命令

本文主要是介绍git不常用初期不太会用的命令,希望你看了能理解这些命令的使用,并在平时使用过程中一点点地刻意进行练习,逐步熟练并知道何时需要用到这些命令去解决你的问题。
( 我也在不断熟练中:D

基础命令

如果你还是刚刚接触git命令,还不清楚 仓库工作流分支提交 的童鞋可以先看下 git使用简易指南,这个应该是我初学git看的第一份且收藏至今的指南了~ 图解很清晰易懂,真10分钟入门的资料:D

然后你会发现如下基础命令将会成为你之后几乎每天都要用到的80%的命令

  • git clone [email protected]:nohosts/nohost.git 克隆远程仓库的内容到本地
  • git pull origin master 获取远程分支master并merge到当前分支
  • git branch -a 查看全部分支(远程+本地)
  • git checkout -b bugFix新建bugFix,并切换到到此分支。(如果分支已存在则去掉-b即可)
  • git status 查看当前~~~~版本状态(是否修改)
  • git add . 增加当前子目录~~~~下所有文件更改至暂存区
  • git commit -m 'xxx' 提交暂存区的修改至本地的版本库, 修改备注为xxx
  • git push 将本地版本推送到远程分支
  • git tag v1.0 dfb02e6e4f2f7b573337763e5c0013802e392818 增加v1.0的tag到某个提交上
  • git merge testBranch 合并testBranch分支至当前分支`
  • git stash 暂存本地的当前修改,将本地代码重置为HEAD状态。(如果需要取出修改,命令后加一个pop即可)
  • git log 显示提交日志(如果想每个提交信息显示在一行,可以加上--pretty=oneline)
  • git show dfb02e6e4f2f7b573337763e5c0013802e392818显示某个提交的详细内容
  • git reset --hard HEAD 将当前版本重置为HEAD

注意这两个命令的区别

git pull = fetch + merge

git pull --rebase = fetch + rebase

“不常用”命令

一、git rebase 变基

在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。 在本节中我们将学习什么是“变基”,怎样使用“变基”,并将展示该操作的惊艳之处,以及指出在何种情况下你应避免使用它。——git-scm变基

说明:后面的举例每个 分支 都有不同的颜色,*前缀 表示现在所处的分支,而 commitid 都由C0、C1、C2代替每一个提交的哈希值,箭头 表示分支的继承

我们之前整合分支用的最多的就是merge了,那merge和rebase有什么区别呢?

1. merge

** merge 合并两个分支时会产生一个特殊的提交记录,它有两个父节点。简单说就是:“我要把这两个父节点本身及它们所有的祖先都包含进来。”**

git checkout master; git merge bugFix

下图中左、右两张图分别是执行如下代码前后的样子:

image

可以看出来,红色圈圈是最主要的改变—— merge 合并分支后,会在master分支上 新增一个C4提交 ,而C4提交里面有master和bugFix代码库所有的修改。

此时的bugFix代码还没和master 同步(颜色不同),我们还需要执行如下代码:

git checkout bugFix; git merge master

image

2. rebase

rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。它的优势就是可以创造更线性的提交历史。

git checkout bugFix; git rebase master

下图中左、右两张图分别是执行代码前后的样子:

image

bugFix 分支里的内容通过 rebase 直接 复制 到 master 分支上。现在 bugFix 分支上的工作在 master 的最顶端,同时我们也得到了一个更 线性 的提交序列。

注意:提交记录 C3 依然存在(树上那个半透明的节点),而 C3' 是我们 rebase 到 master 分支上的 C3 的 副本(内容是一样的,只是commitid更新了)。(如果master和bugFix之间没有其他commit,rebase后commitid不会更新。如果master已经有了自己新的commit,此时rebase后commitid就会更新。)

此时master还没有和bugFix 同步(颜色不同),我们还需要执行如下代码:

git checkout master; git rebase bugFix

image

由于bugFix继承自master,所以 Git 只是简单的把master分支的引用向前移动了一下而已。

3. rebase的延伸用法

3.1 省去切换分支即可rebase

git rebase targetBranch originBranch

表示切换到originBranch,然后执行git rebase targetBranch

3.2 修改某几次提交

git rebase -i commitid

image
如上图标注的,传的commitid为你想修改的提交的 前一个commitid。执行命令后进入vi模式,会提示你一些操作命令(p、r、e...)你只需要在最上方修改默认的pick为你想要的操作,然后退出并wq保存即可生效。

具体操作:

  • pick 使用(啥也没变)

  • reword 使用并修改commit msg, 改后commit id也会更新

  • edit 使用并编辑commit时的文件
    编辑后git add . 然后git commit —amend还可以更新最新的commit msg。 git rebase —continue 把后面的内容加进来并解决冲突, 最后提交。最新的commit id也更新

  • squash 合并commit
    选择最新的commit去合并,然后continue发现这一次和上一次的commit msg都有,你可以删除只留下想要的也可以进行修改 然后 continue和push。如果你不删的话会发现全部文本行都组成了一个多行的commit msg
    如果commit再往前已经没有了 就不能再squash,否则会报错( error: cannot 'squash' without a previous commit )。然后 git rebase --edit-todo 可以继续vi编辑

  • fixup 合并commit到前面而且commit,commit msg也没了

  • drop 删除某个commit

便于理解,补充一个例子,主要是 rebase -i 做了 r,e,s,d操作后产生的结果
image

注意
如果想要恢复这一次rebase操作,则可以执行 git rebase —abort
如果想完全恢复本地分支到远程的状态,可以执行 git reset --hard origin/bugFix ,或者你可以 git reflog 找到对应提交记录回滚,但是有点麻烦

4. rebase需要谨慎使用

当你要改写的commit history还没有被提交到远仓库的时候,也就是说,还没有与他人共享之前,commit history是你私人所有的,那么想怎么改写都可以。

而一旦被提交到远程后,这时如果再改写history,那么势必和他人的history长的就不一样了。git push 的时候,git会比较commit history,如果不一致,commit动作会被拒绝,唯一的办法就是带上 -f 参数,强制要求commit,这时git会以committer的history覆写远程分支,从而完成代码的提交。虽然代码提交上去了,但是这样可能会造成别人工作成果的丢失,所以使用 -f 参数要慎重。

所以,在不用 -f 的前提下,想维持树的整洁,方法就是:在 git push 之前,先 git fetch,再 git rebase

4. 总结

  1. 无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起
  2. 在你自己的分支(非他人共享)的分支进行rebase是可以的,但是如果在公共分支rebase修改提交需要谨慎——最好是先 fetch、再 rebase、最后 push

二、git cherry-pick 选择

cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。`

git checkout master; git cherry-pick C2

下图中左、右两张图分别是执行代码前后的样子:
是不是有点眼熟:D 没错 这个和rebase的效果蛮像的,这两个命令都可以实现复制提交~

image

三、git reset VS revert 回滚

git revert HEAD是用一次新的commit来回滚之前的commit,git reset 是直接向上移动分支,删除一些commit看上去像从未提交一样。这两者看似达到的效果是一样的,其实完全不同。

git reset HEAD~1 
git revert HEAD

如下所见,图1是初始状态(需要撤回 C2 提交),图2和3 是从图1分别执行 resetrevert 后的结果:
image

  1. reset
    执行后,master 分支移回到了 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了
  2. revert
    执行后,在我们要撤销的提交记录 C2 后面多了一个新提交C2',而C2'引入了更改—— 这些更改是用来撤销C2这个提交的。也就是说C2'的状态与C1是相同的。

注意

  • 如果你已经push到线上代码库, reset 删除指定commit以后, 你git push可能导致很多冲突.但是revert 并不会。
  • 如果此回退的分支合并主干分支时,reset 恢复部分的代码依然会出现在历史分支里,但是revert 方向提交的commit 并不会出现在历史分支里。
# 事例
reset后的123 merge了12345 还是12345
revert后的12345(-3) merge了12345 是12345(-3)

四、HEAD^n 和 HEAD~n 相对引用

HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。HEAD 总是指向当前分支上最近一次提交记录
(如果想看 HEAD 指向,可以通过 cat .git/HEAD 查看, 如果 HEAD 指向的是一个引用,还可以用 git symbolic-ref HEAD 查看它的指向。)

1. 基础使用

  • 使用 ^ 表示向上移动 1 个提交记录。
    n表示第n个父提交,不填默认是1(正上方)
  • 使用 ~<num> 向上移动多个提交记录
    如 ~3

注意:操作符还支持链式操,如HEAD^2~3^

2. 延伸用法
移动分支
可以直接使用 -f 选项让分支指向另一个提交。例如下面的命令会将 master 分支强制指向 HEAD 的第 3 级父提交。

git branch -f master HEAD~3

这次主要就总结了这几种“不常用”git命令,希望大家和我都可以多多练习,让他变成你需要时就可以自如使用的“常用”命令!:D

墙裂推荐一个可视化的git练习网站,很易懂好用~

推荐git系列文章

webpack入门必备(一):基础配置

一、webpack的大致构建流程

  1. 初始化参数:在package.json读取对应的命令配置,得出最终参数
  2. 开始编译前的准备:实例化Compiler,加载所有plugin,执行对象的run方法并开始编译。接着根据配置中的entry找到所有入口文件
  3. 编译模块:从入口文件出发,调用所有loader对模块进行编译,再找到模块依赖,重复上述步骤知道所有入口文件都经过处理。在loader处理完所有模块后,得到的每个模块以及它们之间的依赖关系图
  4. 导出:根据入口和模块之间的依赖图,将代码组成一个个包含多个模块的chunk文件加入到输出列表,然后根据配置确定的输出路径和文件名,将chunk导出到项目目录中
    流程图可以参考这里
    webpack打包的规则是,一个入口文件对应一个bundle,该bundle包括了入口文件模块和其他依赖模块,按需加载的模块或者需要单独加载的模块则是分开打包生成其他的bundle。在这些bundle中,有一个较为特殊,就是manifest.bundle.js,被称作webpackBootstrap,它是最先加载的,负责解析webpack生成的其他bundle。

package.json

  1. npm init 初始化文件
  2. script里 可以 NODE_ENV=development 可以设置环境变量,默认是开发模式。
    (我们就将NODE_ENV绑定到了process.env上,并且按以上配置,它所引入的脚本中访问到process.env.NODE_ENV。那我们为什么要设置这个?一般是用做环境判断 if(process.env.NODE_ENV ===xxx)
    另外,使用cross-env可以实现设置时跨平台
  3. 安装依赖
    webpack webpack-cli webpack-devserver

二、webpack总体配置

常用的配置

{
    🌺mode: development/production
    
    🌺entry:  【string | stringArray | object 可以多入口】源文件
            (如果只是string,默认打包后产物叫main.js, object的话产物为键名)
             
    🌺output: { // 输出
        path,
        
        filename: '[name].[chunkhash].js’, //  打包同步代码
        
            chunkFilename: 同上 // 打包异步代码( https://www.cnblogs.com/skychx/archive/2020/05/18/webpack-filename-chunkFilename.html)
            
            publicPath: '//7.url.cn/edu/act/' // 构建出的资源需要异步加载时,加载的cdn地址前缀(小心别写错了导致资源404)
            
            crossOriginLoading: 用于配置JSONP这个异步插入的标签的 crossorigin (如anonymous)
            
        library: 导出库的名字
        
            libraryTarget: 这个一般是封装第三方库时。🌰例如可以配置'commonjs2' 'umd’...
    }

    🌺module: { //【对象】如何处理模块,通过一堆loader
            noParse:不解析的部分。 // 如/moment(?!-)|node_modules\/chart\.js/, (忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能)
            
        rules: [ //【对象数组】
            {
                // ****条件匹配****
                test: 正则匹配文件名或正则数组。🌰例如 [ /\.jsx?$/,/\.tsx?$/],
                
                include: 包含哪些文件(路径)或路径数组
                🌰例如 [
                                       path.resolve(__dirname, 'src'),
                                       path.resolve(__dirname, 'tests'
                                 ],
                                 
                exclude: 排除哪些文件
                🌰例如 path.resolve(__dirname, 'node_modules') ,因为node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换。)
                 
                // **** 应用规则(里面的每一项就是一个loader)****
                use: [ //【字符串数组(则由后往前执行) | 对象数组】
                            {
                                    loader: ‘babel-loader’,
                                    
                                    options:{
                                      cacheDirectory:true,
                                   },
                                    
                                   enforce: 调节这个loader的执行顺序。
                                   🌰例如'post'把该 Loader 的执行顺序放最后
                            }
                 ]
            }
                  ]
    }
    
    🌺resolve: { // 翻译为解析,如何寻找模块
        modules: //【数组】解析第三方模块时应该搜索的目录列表  
            🌰例如 [
                path.resolve(rootDir, 'src'),
                path.resolve(rootDir, 'src', 'node_modules'),
                path.resolve(rootDir, 'node_modules'),
                path.resolve(rootDir, 'src/edu_modules/ke-common/dist/es6'),
            ],

        alias: 【对象】别名
        
        extensions: 【数组】导入时自动帮你补齐列表里的后缀名去找那个文件。
                 🌰例如 ['.jsx', '.js', '.ts', '.tsx'] 
        
        mainFields: 【字符串数组】引用第三方模块是引用他的哪个版本/优先级。
                 🌰例如 ['jsnext:main', 'browser', 'main']  表示ES6, 浏览器,es5
    }
    
    🌺plugins: // 插件数组
        🌰例如 new webpack.DefubePlugin({  创建全局变量
            a: xx
        })
        
    🌺devServer: { // webpack-dev-server配置
        hot: true 模块热替换。
        (DevServer 默认的行为是在发现源代码被更新后会通过自动刷新整个页面来做到实时预览,开启模块热替换功能后将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览。)
        
        host:ip
        
        port: 端口
        
        historyApiFallback: spa中如果刷新页面404,配置这个就可以指定404时跳去什么页面
    }
}

其他配置

{
    🌺target 构建出正对不同运行环境的代码,默认是web
    
    🌺devtool: source-map 开发调试的配置选项(🌰例如, 开发模式用cheap-module-eval-source-map, 既可以生成sourcemap耗时又最短,生产模式用hidden-source-map)
    
    🌺watch: true 监听文件更新,文件改变后重新编译。平常默认是关闭的,但是使用devServer时,默认开启。
    
    🌺externals: 不用重复再去打包的依赖.
            🌰例如,一个npm包引用的react包不应该打到包里),或者cdn引入的资源不需要打包
             {
               jquery: 'jQuery',
                react: 'React',
                'react-dom': 'ReactDOM',
                // 'react-dom/server': 'ReactDOMServer',
              },

    🌺optimization: //【对象】优化(webpack4之后不用自己配置,)
}

三、module的常用loader及使用

3.1 babel 把ES6转换为ES5、ES3 (babel-loader)

  • 最新ES 语法:比如,箭头函数
  • 最新ES API:,比如,Promise
  • 最新ES 实例方法:比如,String.protorype.includes

参考文章:

3.1.1 babel 6部分配置(.babelrc)

modules:默认使用 "commonjs"。即将代码中的ES6的import转为require。

  • @babel/preset-es2015:按照ES6标准编译
  • @babel-stage-x: 处理尚处在提案语法的插件集合,babel@7已经不推荐使用, stage-0的功能范围最大(通常使用建议配到@babel/preset-stage-2)
  • @babel-polyfill: api只能被polyfill
  • @babel-runtime + 插件@babel-plugin-transform-runtime配套使用。提取所有页面所需的helper函数到一个包里,避免重复注入(如打包后会变成require('babel-runtime/helpers/createClass'))

🌰事例:

{
  "sourceMaps": true,
  "presets": ["es2015","stage-2","react"], // 源码使用了哪些语法特性,需要提供支持.其实是一组plugins的集合
   "plugins": [ // 插件,控制如何转换代码
    [
      "transform-runtime", // transform-runtime 默认会自动的为你使用的 ES6 API 注入 polyfill, polyfill 的注入应该交给模块使用者,因为使用者可能在其它地方已经注入了其它的 Promise polyfill 库。所以关闭该功能
      {  "polyfill": false  } 
    ]
   ],
}

3.1.2 babel 7的部分配置(babel.config.js)

简单说,大部分babel 6的依赖命名从-改成了/

  • @babel/core 必需 根据我们的配置文件转换代码

  • @babel/present-env 相当于一个处理es6+规范语法的插件集合。 根据运行环境为代码做相应的编译, 包括了所有ECMA标准(bebal-preset-esxxxx),不包含state-x一些列插件,只支持最新推出版本的JavaScript语法(stage-4),如果需要支持其他stage需要额外引入。stage0表示最没定稿的新特性
    image

  • @babel/runtime + @babel/plugin-transform-runtime 配套使用。 沙箱,提取所有页面所需的helper函数到一个包里,避免重复注入

🌰事例:

{
    // "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-destructuring"]    // 一个个插件不如使用preset-env就可以一次性全部引入
    // "presets": ["@babel/preset-env"]
    
    "presets": [
    "@babel/preset-env", 
    {
               "modules": false,
               "useBuiltIns": "entry", // 按需引入:在入口处把所有ie8以上浏览器不支持api的polyfill引入进来
                                       // 'usage',其功能更为强大,它会扫描你的代码,只有你的代码用到了哪个新的api,                        它才会引入相应的polyfill。【试验状态,谨慎使用】
               "targets": "ie >= 8" // 只有ie8以上版本浏览器不支持的语法才会被转换
    }
    '@babel/preset-react',
    '@babel/preset-typescript',
    ],
    "plugins": [
        "@babel/plugin-syntax-dynamic-import", //动态导入
        ["@babel/plugin-transform-runtime", {
            "corejs": 2
        }]                                // 所有的helper函数抽离到一个包中,由所有的文件共同引用则可以减少可观的代码量。也可以为你的代码创建一个sandboxed environment(沙箱环境),这在你编写一些类库等公共代码的时候尤其重要。
    ] 
}

3.2 TS转换成js (awesome-typescript-loader)

🌰tsconfig事例:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "baseUrl": ".",
    "paths": {
      "assets/*": ["src/assets/*"],
      "components/*": ["src/components/*"],
    },
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "lib": ["es6", "es2017", "dom", "dom.iterable", "scripthost"],
    "target": "es5", // 编译出的代码使用ES几
    "module": "ESNext", // 编译出的代码采用的模块规范
    "jsx": "react",
    "noUnusedLocals": true,
    "typeRoots": ["node_modules/@types", "src/assets/types"],
    "sourceMap": true,
    "importHelpers": true, // 避免helper函数重复引入
    "types": ["jest", "node"]
  },
  "include": ["src/**/*"],
  "exclude": ["src/edu_modules/**/*", ".template/**/*"]
}

3.3 react解析

实现:

  • 方法1:结合babel,config里直接presets配置react
  • 方法2:结合ts,config里配置"compilerOptions": {
    "jsx": "react" // 开启 jsx ,支持 React
    }
    但是需要入口文件改为.tsx,并且安装types/react @types/react-dom

🌰方法1事例:

{
  "presets": ["es2015", "stage-2", "react"],
  "plugins": [
    "react-hot-loader/babel",
    "transform-function-bind",
    "transform-class-properties",
    "transform-export-extensions",
    ],
    "env": {
      "backend": {
        "plugins": [
          [ "webpack-loaders",
            { "config": "./webpack.config.babel.js"
            , "verbose": true
            }
          ]
        ]
      }
    }
  }
}
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        user: {
          loader: 'babel-loader?cacheDirectory=true', // cacheDirectory用于缓存babel的编译结果,加快重新编译的速度
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              '@babel/plughin-transform-runtime',
              '@babel/plugin-transform-modules-commonjs'
            ]
          }
        }
      }
    ]
  }
}
  • 对于普通项目,可以直接使用preset-env配置polyfill
  • 对于类库项目,推荐使用@babel/runtime,需要注意一些实例方法的使用

3.4 样式相关的loader

loader的配置是从右往左的,且配置时可以省略“-loader”
(如 style-loader css-loader postcss-loader/sass-loader)

  • sass-loader // 解析sass为css
  • less-loader // 解析less为css
  • css-loader // 解析css。如@import,url()的导入语句 + 压缩css
  • style-loader, // 将解析后的样式嵌入js代码中。(将require引入的样式嵌入js文件中,有好处也有坏处。好处是减 少了请求数,坏处也很明显,就是当你的样式文件很大时,造成编译的js文件也很大。如果css文件较大时,可以可以用extract-text-webpack-plugin分开css文件和js,这样css和js并行加载加快加载速度)
  • postcss-loader + autoprefixer插件,可以为我们的样式添加前缀-webkit-,-ms- 提高兼容性
    静态资源相关loader
  • url-loader 将图片转换为base64后注入到js和css中。(注意:会影响打包后文件大小,从而影响加载速度)
🌰事例:
{
        test: /\.png$/,
        use: [{
          loader: 'url-loader',
          options: {
            // 30KB 以下的文件采用 url-loader
            limit: 1024 * 30,
            // 否则采用 file-loader,默认值就是 file-loader 
            fallback: 'file-loader',
          }
        }]
      }
  • file-loader CSS 中导入图片的语句替换成正确的地址,并同时把文件输出到对应的位置。(比如css和js中把url相对路径的图片输出到dist文件夹,并取另一个带hash的名字)
🌰事例:
{
        test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf|svg|xlsx)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[path][name].[ext]',
            },
          },
        ],
      },
  • svg可以用图片用的file-loader和url-loader, 也可以用raw-loader ,svg-inline-loader。后面两种是应用于js引入时,输出是<svg xxx>

参考文章:
zhengweikeng/blog#9

四、plugin插件

  • webpack-merge:提取公共配置,分离生产环境和开发环境的配置文件。clean-webpack-plugin:每次编译前自动清空dist目录
  • html-webpack-plugin:从html模板自动生成最终的html,它的AutoWebPlugin插件可以生成多页面应用
  • defineplugin:声明process.env.NODE_ENV的值
  • extract-text-webpack-plugin:从js提取css到css文件
  • optimize-css-assets-webpack-plugin:合并相同的css样式文件
  • css-split-webpack-plugin:拆分过大的csss文件
  • mini-css-extract-plugin:分开打包css文件
  • webpack-md5-hash:webpack自身生成文件hash值的方法是不确定的,为了确保hash值是根据文件内容生成的,可以使用该插件

五、resolve优化

  • resolve.extensions:使用require或import引入文件时可以省略后缀:
  • resolve.alias:简化引用路径:
  • resolve.mainFields:通过第三方插件的package.json中main字段(大多数第三方模块都采用这个字段)中的地址来引用文件
  • resolve.modules:限制引入第三方包的范围
  • resolve.mainFiles:在一定范围下默认优先查找的文件名

六、其他优化

  • optimization抽离公共组件代码
  • DllPlugin减少基础模块的编译次数
  • IgnorePlugin忽略某些模块打包
  • CopyWebpackPlugin生产环境中将提前构建的包同步到dist
  • uglifyjs去js代码压缩
  • web-webpack-plugin使用静态资源CDN
  • HappyPack进行多进程打包
  • webpack-bundle-analyzer打包结果进行分析
    具体可以阅读《深入浅出webpack》

后记

因为webpack的知识点很多也很细,推荐阅读《深入浅出webpack》
这本书系统性,渐进性地讲了webpack的基础配置,项目实战,打包优化,插件及运行原理。如果想快速学习webpack并入门可以看看这本书,会比官网看起来更加有重点一些,但注意的是有些配置会有点过时了。
然后初学者可以多上手试试,这样印象更深刻也更能理解,我也在学习中~

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.