webpack的配置

webpack也用了好久,想来短期内前端暂时也离不开它。虽然现在很多框架都集成了,但难免还是会遇到需要自己配置的时候。在这里记录下一些基础配置。
以下基于 webpack5,不过一般差别不大

const path = require('path')
const { execSync } = require('child_process')
const webpack = require('webpack')
const Customplugin = require('./custom-plugin')

// 默认也会覆盖,不过我习惯删了,windows不支持命令的话,可以改用 fs 模块
execSync(`rm -rf build`)

module.exports = {
  context: process.cwd(), // 当前上下文,一般也不用配置
  mode: 'development', // 打包模式,production模式自动压缩混淆
   // source-map 类型 
   // https://webpack.js.org/configuration/devtool/#root
  devtool: false,
   // 编译信息,一般也不需要那么多,可以精细配置 
   // https://webpack.js.org/configuration/stats/#root
  stats: 'none',
  entry:  { 
    a: __dirname + '/a.js',
    b: __dirname + '/b.js',
    c: __dirname + '/c.js'
  }, // 指定文件入口
  output: {
    path: __dirname + '/build',// 打包后的文件存放的地方
    filename: '[name].js'// 打包后输出文件的文件名,比如上面打出来就会是 a.js b.js c.js
  },
  // externals:不打包某些库 https://webpack.js.org/configuration/externals/#externals
  optimization: {
    minimize: false, // 不要压缩
     // 抽出公共js,贼复杂,一般用默认的就可以了 
     // https://webpack.js.org/plugins/split-chunks-plugin/#root
    splitChunks: {
      chunks: 'all',
      // minChunks: 3,
      // minSize: 0,
      cacheGroups: {
        default: {
          priority: 1,
          reuseExistingChunk: true,
          enforce: true
        }
      }
    }
  },
  module: {
    rules: [
      { // 添加自定义 loader
        loader: './custom-loader.js',
        options: {
          param: 1
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
        options: {
          cacheDirectory: true,
          presets: ['@babel/preset-env'],
          plugins: ['@babel/transform-runtime']
        }
      },
      {
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            }
          },
          {
            loader: 'less-loader',
            options: {
              noIeCompat: true
            }
          }
        ]
      }
    ]
  }, 
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"development"',
    }),
    new Customplugin({ params: 1 }) // 添加自定义 plugin
  ],
  resolve: { // 配置一系列搜索资源的规则
    alias: { // 起别名,方便资源引用
      @src: path.resolve(process.cwd(), 'src')
    },
    // 引用的文件可以不加后缀,会找这些后缀的文件
    extensions: ['.wasm', '.mjs', '.js', '.json'],
    // 告诉 webpack 搜索哪些文件夹,可以用绝对路径写死
    modules: ['node_modules'],
    // 从npm包导入时,例如import * as D3 from 'd3',此选项将确定package.json选中其中的哪些字段
    mainFields: ['browser', 'module', 'main'],
    // 解析目录时要使用的文件名,一般都是 index 了,不要瞎改
    mainFiles: ['index']
  }
}

自定义loader:

const loaderUtils = require('loader-utils')

// https://webpack.js.org/api/loaders/
module.exports = function (content, map, meta) {
  console.log(this.data.value) // 123,具体场景看文档
  // 用 loaderUtils 解析这个 loader 的参数
  const options = loaderUtils.getOptions(this)

  // 最后要以一个模块的导出形式
  content = `module.exports = function () { ${content} }`

  // 同步
  this.callback(null, content, map, meta)
  return// 当调用 callback() 时总是返回 undefined

  // 同步
  // return content

  // 异步
  // const callback = this.async()
  // callback(null, result, map, meta)
}

// 可以定义一个 pitch 方法,要写在 loader 方法下面,但是会先于 loader 方法执行
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 123
}

自定义plugin:

const pluginName = 'CustomPlugin'

// https://webpack.js.org/api/plugins/
// 写成 ES5 prototype 形式也可以,一定要有 apply 方法
class CustomPlugin {
  constructor (options) {
    this.options = options
  }
  apply(compiler) {
    // compiler 和 compilation 有很多生命周期钩子
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('The webpack build process is starting!!!')
    })
  }
}

module.exports = CustomPlugin

深度优先遍历(DFS)-栈和广度优先遍历(BFS)-队列的理解

JS版本的,概念什么的就不赘述了。参考了这位大佬的文章(JS算法之深度优先遍历(DFS)和广度优先遍历(BFS))
在这里记录下自己的理解,为啥一个是栈,一个是队列。看码:

<div id="box">
  <ul>
    <li><div><p></p></div></li>
    <li><img /></li>
    <li><a></a></li>
  </ul>
  <div>
    <p>1</p>
    <p><span><strong></strong></span></p>
    <p>3</p>
  </div>
  <button><strong></strong></button>
</div>
// 深度优先,用的是栈,同一边进出,后入先出
function deepFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var stack = []
    stack.push(node) // 第一个元素无论 push 还是 unshift 都一样,这里只是为了相呼应。
    while(stack.length != 0) {
      // 最核心的差别在这里了
      // 第一次进来:是[$box],弹出来后都是为空
      // 第二次进来: [button, div, ul],然后 ul 被 pop 出去,收进结果数组 nodes 里
      var item = stack.pop()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        // 这一步也是一样,把每个子节点放进去待遍历数组
        // 把 $box 的子节点放进待选数组,此时是 [button, div, ul]
        // 第一次 while 进来都是一样
        // 这里先收集左边还是右边的子级意义都是一样的
        // 这里是从先把 button 放进来,ul 最后,等下 pop 出去就是 ul 最先
        stack.push(children[i])
        // 点出 ul 的子级,收进 stack 的最后,下次 while 进来,pop 就会是 ul 的子级,也即是 li
        // 然后再继续下去,还是会把 ul 的子级的子级,也就是 div,收进 stack 最后
        // 也就是不断的子级优先,达成深挖
      }
    }
  }
  return nodes
}

// 广度优先,用的是队列,一边进另一边出,先入先出
function breadthFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var queue = []
    // 第一次进来:是[$box],弹出来后都是为空
    // 第二次进来:[button, div, ul],然后 button 被 unshift 出去,收进结果数组 nodes 里
    // 第三次进来:[div, ul, strong],然后 div 被 unshift 出去,收进结果数组 nodes 里
    // 第四次进来:[ul, strong, p, p, p],然后 ul 被 unshift 出去,收进结果数组 nodes 里
    // 可以看出跟深度的差别,点出子级都是排进待选数组的最后
    // 但是深度是栈pop,加塞在数组最后的子级优先
    // 广度是队列unshift,一起放进数组的兄弟级优先
    queue.unshift(node)
    while(queue.length != 0) {
      var item = queue.shift()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        queue.push(children[i])
        // 对应上面第二次进来:点出 button 的子级,也就是 strong,收进 queue 的最后
        // 但对 unshift 的结果没影响,下次 while 进来,unshift 依然是 div
        // 对应上面第三次进来:点出 div 的子级,也就是 p,收进 queue 的最后
      }
    }
  }
  return nodes
}

可以看出,就是在遍历时候暂存的数据结构不一样。
深度优先,就是一直深挖,孩子的孩子的孩子…。
广度优先,就是兄弟兄弟兄弟孩子兄弟…。需要注意的是,同级的就算兄弟。意思是相对于根级为孙子辈,就算不是同个父级,也算是兄弟级。

2022-10-18 21:31:18
更新了一篇用 js 做遍历的《深广度优先遍历》

记录一些基础算法

开始补充一些算法和数据结构的知识

冒泡排序

const arr = [162, 6, 3, 8, 90, 234, 5, 7, 31, 45, 12, 1, 98, 23, 87, 40]
function sort(arr) {
  const len = arr.length - 1
  for (let x = 0, y = len; x < y; x++) {
    // 把第一个跟后面那个对比,如果比后面的数大,就交换位置,到最后就可以确定最大的数在最后
    // 也就是在 length 的位置
    // 在第一轮的时候,x=1,也就是遍历到倒数第二个就好,因为取到 i+1 的数
    for (let i = 0, l = len - x; i < l; i++) {
      const first = arr[i], second = arr[i+1]
      if (first > second) {
        [arr[i], arr[i+1]] = [second, first]
      }
    }
    // 假设没有第一层,最大的数已经在最后,那么第二轮就是要在剩下的数找到最大,并放到倒数第二位置
    // 也就是在 length-1 的位置
    // 此时 x=2,因为依然是当轮最后数(第二轮为length-1)的前一个
    // for (var i = 0, l = arr.length - 2; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 第三轮 x=3
    // for (var i = 0, l = arr.length - 3; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 第四轮 x=4
    // for (var i = 0, l = arr.length - 4; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 所以循环个遍,就成了外面一层的循环
  }
  return arr
}
console.log(sort(arr))

二分法

就是在一个有序的序列中迅速找到要找的那个数的索引。一个一个遍历当然也可以找到,但是不确定性导致复杂度。
二分法就是中间砍一刀,这样数列就成了两半。看看中间值是不是,是那就刚好了。不是的话,判断目标数是在前一半还是后一半,如此这般的循环即可。

这种思想应该在现实生活中还是经常用到的,比如git bisect看看大佬的介绍)。
以及,让我想起以前 TVB 的《超级无敌掌门人》里面有个游戏叫“超级无敌开口中”,轮流在在100里面瞎说一个数,中了就受罚。特别是轮到说最后一个必中的数字时那种视死如归的神情,超搞笑。

function getRandomNum(numMin, numMax) {
  return Math.round( Math.random()*(numMax-numMin) + numMin )
}  
function getArr() {
  const arr = []
  for(let i = 1, l = 100; i <= l; i++) {
    arr.push(i)
  }
  return arr
}
// 随机生成 1 ~ 100 中的某数
const num = getRandomNum(1, 100)
// 生成 1 - 100 的数组
const arr = getArr()
function binarySearch(num, arr) {
  let start = arr.length
  let end = 0
  const getHalf = () => {
    let length = start + end
    return (length % 2 === 0) ? (length / 2) : (length + 1) / 2
  }
  // 取中值
  let half = getHalf()
  let current = arr[half]
  while (current !== num) {
    if (current > num) {
      start = half
    } else if (current < num) {
      end = half
    }
    half = getHalf()
    current = arr[half]
  }
  return half
}
const idx = binarySearch(num, arr)
console.log(idx, arr[idx], num)

斐波那契数列

具体的数学意义就不说了,我也不懂,总而言之是满足这样的一个数列:F(0)=0,F(1)=1,F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)

// 第一反应就是递归,最简单最明晰。但效率最低,其实就是每次算出来的值没存起来,导致每个 n 位的值传进去都要重新递归的算
function fibonacci(n) {
  if(n === 0 || n === 1) {
    return n
  }
  return fibonacci(n-1) + fibonacci(n-2)
}
// 改进一些,是个数列自然就可以用个数组来存
// 把之前获得 n 位的值存起来,下次要拿就直接取就好
function fibonacci2(n) {
  if(n === 0 || n === 1) {
    return n
  }
  const arr = [0, 1]
  for(let i = 2; i <= n; i++) {
    arr[i] = arr[i-1] + arr[i-2]
  }
  return arr[n]
}
// 改进一些,其实每次都是需要上两位就可以了,不需要用整个数组来存全部
function fibonacci3(n) {
  if(n === 0 || n === 1) {
    return n
  }
  let lastOne = 1 // n-1
  let lastTwo = 0 // n-2
  for(let i = 2; i <= n; i++) {
    var current = lastOne + lastTwo
    // n-2 和 n-1 分别获取后一位的值
    lastTwo = lastOne
    lastOne = current
  }
  return current
}
// 第一种递归的方法,在浏览器 40 以后基本快跑不出来了
// 第三种还是比第二种快那么些许
console.time('fibonacci')
console.log(fibonacci(40))
console.timeEnd('fibonacci')

玩一玩puppeteer

puppeteer官网上写着:

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。

其实就是个浏览器,给了许多方法让我们可以控制这个浏览器,比如打开某某网页,输入什么字符,点击哪个按钮。看起来就像是用程序来模拟人工的行为。
这样就表示了它可以用来自动化测试。搭配 jest 或 mocha 之类的测试框架,进行e2e测试,模拟一个人去访问网站,进行一些操作。
当然不止这一个方案,还有 selenium 和在这之上的 night-watch,也是干 web 自动化测试的活。

我们项目上使用它来跑自动化测试的,我自己用它来做爬虫,奸笑~
可能也不叫爬虫,就是上新闻网站扫一下热搜,到图片库搜点图片之类的。

安装:

安装是个蛋疼的事,因为这个用到chromium,这玩意在谷歌仓库。所以如果不能科学上网,那就只能修改为淘宝源。

env puppeteer_download_host=https://npm.taobao.org/mirrors npm i puppeteer

npm config set puppeteer_download_host  https://npm.taobao.org/mirrors
npm i puppeteer

年代久远,记得都是可以的。不保证时效性…

我们项目一开始使用它来截图

const puppeteer = require('puppeteer')
;(async () => {
  const browser = await puppeteer.launch({
    headless: true // 无头就是不要弹出浏览器
  })
  const page = await browser.newPage()
  await page.goto('https://www.baidu.com')
  await page.setViewport({
    width: 1200,
    height: 800
  })
  await page.screenshot({
    path: 'puppeteer.png',
    fullPage: true
  })
  await browser.close()
})()

来看下用它来搜百度热搜,之所以对 puppeteer 感兴趣,就是同事写了一篇这个教程,里面就是演示了爬热搜。

const puppeteer = require('puppeteer')
;(async () => {
  const browser = await puppeteer.launch({
    headless: true // 无头就是不要弹出浏览器
  })
  const page = await browser.newPage()
  await page.goto('http://top.baidu.com/buzz?b=1&fr=topindex', {
    timeout: 0, // 设置超时时间为0
    waitUntil: 'domcontentloaded' // 有多个状态可设置,这里设置为 dom 渲染完成
  })
  // 有时候网速卡,加载慢,容易超时报错,所以上面把超时时间去掉
  // 同时网页渲染也需要时间,可以等一下,
  // 比如 await page.waitFor(1000) 等待1000ms
  // 比如我之前爬图片的时候,都知道图片是相当耗时的
  // 所以我写成等图片的 dom 可见即可,可见包括了它已经被赋予 src,不需要等到图片下载并渲染完成
  // 然后获取其 src,再通过别的方法下载
  // await page.waitForSelector(dom, { visible: true, timeout: 0 })
  const list = await page.evaluate((dom) => {  // 爬取内容
    // 这个 dom 需要去网站上看实际结构,很好找,跟 jQuery 一样
    const $doms = document.querySelectorAll('.mainBody .keyword a')
    const list = Array.from($doms).map(($dom) => {
      return {
        href: $dom.href,
        text: $dom.innerText
      }
    })
    return list
  })
  // 这个 list 就记录了热搜的标题和网址
  await page.close()
  await browser.close()
})()

对网页的抓取还有这个cheerio,在 node 里面像 jQuery 一样获取dom。
不过好像对单页面的网站比较麻烦。因为是抓了整个网页内容下来,再来分析页面。那对于 SPA 抓下来也是一个容器一个js,没有实体dom。所以具体看用途啦。

puppeteer还有很多牛的功能,这里只是用到了冰山一角,还能做更多有趣的事。

在海边

最近去了一趟深圳大鹏官湖村。典型的海边小镇,地方不大,沙滩挺大。有一个路口在滤镜加持下,居然有点像灌篮高手片头里,樱木在车站前的经典场景。