每周两个小目标

87 阅读9分钟

唯有量变能引起质变 还望给自己一个交代

浏览器相关:

进程:cpu调动资源的最小单位

线程:cpu调度的最小单位

浏览器是多进程的,默认一个网页(tab)是一个进程

浏览器进程(主进程,唯一) 渲染进程 gpu进程 第三方插件进程

前端最关注的是渲染进程,页面的渲染,JS的执行,事件的循环均在此,为多线程

  • gui渲染线程 负责(html树 render树 布局绘制这个流程),与js线程互斥
  • js线程 负责解析js并运行
  • 事件触发线程
  • 异步http请求线程
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 

这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window

浏览器与node的事件循环:

浏览器事件循环: 只有宏任务和微任务队列,执行完成当前宏任务后清空微任务队列,交替进行

Node事件循环: 基于libuv (读音同library)的事件循环,

node有六个事件队列:timer,IO,check,close (宏)nextTick,promise(微)

在Nodejs中,事件循环分为6个阶段。每个阶段都有一个任务队列

image.png

  • 事件循环依次进入并执行timer queue、IO queue、check queue、close queue。

  • 事件循环进入下一阶段前,都需要先清空微任务队列。

  • 每个宏任务执行完,都需要检查微任务队列是否有任务,如果有微任务,则先清空微任务,再执行下一个宏任务。

http相关:

webpack相关:

热更新 hot module replacement

首先启动用户端服务器以及一个websocket双向通信服务器,然后以watch模式开始编译,每次编译都会产生一个唯一的hash值,首次编译的代码为h1,当源代码发生变化为h2时,服务端会将h2发给客户端,客户端随后将自己当前版本h1发送给服务端,服务端比较h1与h2的差别之后将具体的变更代码返还给客户端。

客户端拿到代码后, 重新去执行依赖该模块的模块(比如 src/name.js 被修改了,src/index.js 依赖 src/name.js,那就要重新执行 src/index.js 这个模块),达到更新的目的。

模块联邦

模块联邦旨在解决微前端架构中不同项目的代码和模块复用问题,让他们利用cdn在运行时动态共享,而不是构建时静态的打包

A项目导出组件(expose),B项目直接引用(remotes进来)

共享依赖库。文件名格式为依赖库名称_at_版本.async.js Host应用和Remote应用经打包后都会生成此类文件。远程组件在Host中加载时,会优先引用Host中的同名文件,以达到共享依库赖的效果。

加载远程模块是一个异步操作,

主要提供功能(使用场景): 应用间的组件共用,依赖共享。

vite能引用webpack分享的组件,但webpack不能引用vite分享的组件,vite之间能互相引用。
原因:vite打包出来的chunk,浏览器请求完无法直接解析,而联邦模块说到底就是通过浏览器请求这份chunk,然后解析。
解决办法:​

  1. 使用webpack打包vite项目。
  2. 使用插件,浏览器请求完这个chunk后,通过插件去解析。

打包原理与流程

与vite的对比和差异

详解vite

zhuanlan.zhihu.com/p/400313956

webpack需要做一个全量的打包,生成入口文件,编译后在浏览器加载

vite首先使用esbuild做一个预构建提前将依赖转为esm模块,之后按照这个预构建按需编译,省去了第一步庞大的打包流程

将应用中的模块区分为 依赖(node_modules) 和 源码(项目代码) 两类;模块之间的依赖关系的解析由浏览器实现。Vite 只需要在浏览器请求源码时进行转换并按需提供源码

充分利用协商缓存304,热更新直接重新请求文件即可

vite缺点:开发环境下首屏加载变慢,因为unbundle机制,将webpack在 dev server 启动过程中完成的工作loader,parse,transfom等等转移到了 dev server 响应浏览器请求的过程中

懒加载性能变慢

vite在开发环境中使用即时编译,打开入口文件后根据依赖关系通过浏览器按需加载。

vite在开发环境通过rollup打包出一个或多个bundle文件以便在生产环境使用。rollup会对代码进行压缩和优化。

常用的vite配置项

{
  root: process.cwd(), // 项目根目录(index.html 文件所在的位置),
  base: '/', // 开发或生产环境服务的公共基础路径 配置引入相对路径
  mode: 'development', // 模式
  plugins: [vue()], // 需要用到的插件数组
  publicDir: 'public', // 静态资源服务的文件夹
  cacheDir: 'node_modules/.vite', // 存储缓存文件的目录
  resolve: {
    alias: [ // 文件系统路径别名
      {
        find: //@//,
        replacement: pathResolve('src') + '/'
      }
    ],
    dedupe: [], // 强制 Vite 始终将列出的依赖项解析为同一副本
    conditions: [], // 解决程序包中 情景导出 时的其他允许条件
    mainFields: [], // 解析包入口点尝试的字段列表
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], // 导入时想要忽略的扩展名列表
    preserveSymlinks: false, // 启用此选项会使 Vite 通过原始文件路径确定文件身份
  },
  css: {
    modules: {
      scopeBehaviour: 'global' | 'local',
      // ...
    },
    postcss: '', // 内联的 PostCSS 配置 如果提供了该内联配置,Vite 将不会搜索其他 PostCSS 配置源
    preprocessorOptions: { // css的预处理器选项
      scss: {
        additionalData: `$injectedColor: orange;`
      }
    }
  },
  json: {
    namedExports: true, // 是否支持从.json文件中进行按名导入
    stringify: false, //  开启此项,导入的 JSON 会被转换为 export default JSON.parse("...") 会禁用按名导入
  },
  esbuild: { // 最常见的用例是自定义 JSX
    jsxFactory: 'h',
    jsxFragment: 'Fragment'
  },
  assetsInclude: ['**/*.gltf'], // 指定额外的 picomatch 模式 作为静态资源处理
  logLevel: 'info', // 调整控制台输出的级别 'info' | 'warn' | 'error' | 'silent'
  clearScreen: true, // 设为 false 可以避免 Vite 清屏而错过在终端中打印某些关键信息
  envDir: '/', // 用于加载 .env 文件的目录
  envPrefix: [], // 以 envPrefix 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中
  server: {
    host: '127.0.0.1', // 指定服务器应该监听哪个 IP 地址
    port: 5000, // 指定开发服务器端口
    strictPort: true, // 若端口已被占用则会直接退出
    https: false, // 启用 TLS + HTTP/2
    open: true, // 启动时自动在浏览器中打开应用程序
    proxy: { // 配置自定义代理规则
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    },
    cors: true, // 配置 CORS
    force: true, // 强制使依赖预构建
    hmr: { // 禁用或配置 HMR 连接
      // ...
    },
    watch: { // 传递给 chokidar 的文件系统监听器选项
      // ...
    },
    middlewareMode: '', // 以中间件模式创建 Vite 服务器
    fs: {
      strict: true, // 限制为工作区 root 路径以外的文件的访问
      allow: [], // 限制哪些文件可以通过 /@fs/ 路径提供服务
      deny: ['.env', '.env.*', '*.{pem,crt}'], // 用于限制 Vite 开发服务器提供敏感文件的黑名单
    },
    origin: 'http://127.0.0.1:8080/', // 用于定义开发调试阶段生成资产的 origin
  },
  build: {
    target: ['modules'], // 设置最终构建的浏览器兼容目标
    polyfillModulePreload: true, // 是否自动注入 module preload 的 polyfill
    outDir: 'dist', // 指定输出路径
    assetsDir: 'assets', // 指定生成静态文件目录
    assetsInlineLimit: '4096', // 小于此阈值的导入或引用资源将内联为 base64 编码
    cssCodeSplit: true, // 启用 CSS 代码拆分
    cssTarget: '', // 允许用户为 CSS 的压缩设置一个不同的浏览器 target 与 build.target 一致
    sourcemap: false, // 构建后是否生成 source map 文件
    lib: {}, // 构建为库
    manifest: false, // 当设置为 true,构建后将会生成 manifest.json 文件
    ssrManifest: false, // 构建不生成 SSR 的 manifest 文件
    ssr: undefined, // 生成面向 SSR 的构建
    write: true, // 启用将构建后的文件写入磁盘
    emptyOutDir: true, // 构建时清空该目录
    brotliSize: true, // 启用 brotli 压缩大小报告
    chunkSizeWarningLimit: 500, // chunk 大小警告的限制
    watch: null, // 设置为 {} 则会启用 rollup 的监听器
    // minify:false, // 表示打包后的文件内容不进行压缩,方便阅读
    minify: "terser", // esbuild | terser 指定使用哪种混淆器
    terserOptions: {
        compress: {
            // 打包的时候可以移除console和debugger
            drop_console: true,
            drop_debugger: true,
       },
    }, // 传递给 minify: "terser" 的更多 minify 选项
    preview: {
        port: 5000, // 指定开发服务器端口
        strictPort: true, // 若端口已被占用则会直接退出
        https: false, // 启用 TLS + HTTP/2
        open: true, // 启动时自动在浏览器中打开应用程序
        proxy: { // 配置自定义代理规则
          '/api': {
            target: 'http://jsonplaceholder.typicode.com',
            changeOrigin: true,
            rewrite: (path) => path.replace(/^/api/, '')
          }
        },
        cors: true, // 配置 CORS
   },
   optimizeDeps: {
    entries: [], // 指定自定义条目——该值需要遵循 fast-glob 模式
    exclude: [], // 在预构建中强制排除的依赖项
    include: [], // 可强制预构建链接的包
    keepNames: false, // true 可以在函数和类上保留 name 属性
  },
  ssr: {
    external: [], // 列出的是要为 SSR 强制外部化的依赖,
    noExternal: '', // 列出的是防止被 SSR 外部化依赖项
    target: 'node', // SSR 服务器的构建目标
  },
  rollupOptions: { // 自定义底层的 Rollup 打包配置
      output: {
        chunkFileNames: 'js/[name]-[hash].js', // 打包后的文件名称
        entryFileNames: 'js/[name]-[hash].js', // 打包后的入口文件名称
        // assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等 指定静态资源文件名(不含导出的代码)
        // assetFileNames:"[name][extname]",
        // 对打包出来的资源文件进行分类,分别放到不同的文件夹内
        // assetFileNames(chunk) {
        //   console.log(chunk.name);
          
        //   // css
        //   if (chunk.name?.endsWith('.css')) {
        //     return 'css/[name]-[hash].[ext]';
        //   }
        //   // image
        //   if (/.(png|jpg|gif|jpeg|webp|svg)$/.test(chunk.name || '')) {
        //     return 'images/[name]-[hash].[ext]';
        //   }

        //   return `other/[name]-[hash].[ext]`;
        // },

        // 对打包出来的资源文件进行分类,分别放到不同的文件夹内
        assetFileNames(assetsInfo) {
          //  css样式文件
          if (assetsInfo.name?.endsWith(".css")) {
            return "css/[name]-[hash].css";
          }
          //  字体文件
          const fontExts = [".ttf", ".otf", ".woff", ".woff2", ".eot"];
          if (fontExts.some((ext) => assetsInfo.name?.endsWith(ext))) {
            return "font/[name]-[hash].[ext]";
          }

          //  图片文件
          const imgExts = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".icon"];
          if (imgExts.some((ext) => assetsInfo.name?.endsWith(ext))) {
            return "img/[name]-[hash].[ext]";
          }

          //  SVG类型的图片文件
          const imgSvg = [".svg",];
          if (imgSvg.some((ext) => assetsInfo.name?.endsWith(ext))) {  
            return "assest/icons/[name].[ext]";
          }

          //  视频文件
          const videoExts = [".mp4", ".avi", ".wmv", ".ram", ".mpg", "mpeg"];
          if (videoExts.some((ext) => assetsInfo.name?.endsWith(ext))) {
            return "video/[name]-[hash].[ext]";
          }
          //  其它文件: 保存在 assets/图片名-哈希值.扩展名  
          return "assets/[name]-[hash].[ext]";
        },
        // 打包的文件进行拆包处理/静态资源分拆打包
        manualChunks:(id)=>{
          // 这个ID,就是所有文件的绝对路径
          if(id.includes("node_modules")){
            // 因为 node_modules 中的依赖通常是不会改变的
            // 所以直接单独打包出去
            // 这个return 的值就是打包的名称
            return "vendor";
          }
       },
       // 或者 
       manualChunks(id) { //静态资源分拆打包
          if (id.includes('node_modules')) {
            return id.toString().split('node_modules/')[1].split('/')[0].toString();
          }
       }
  }, 
}

对splitChunk的配置

module.exports = {
  //...
  optimization: {
    splitChunks: {
      //在cacheGroups外层的属性设定适用于所有缓存组,不过每个缓存组内部可以重设这些属性
      chunks: "async", //将什么类型的代码块用于分割,三选一: "initial":入口代码块 | "all":全部 | "async":按需加载的代码块
      minSize: 30000, //大小超过30kb的模块才会被提取
      maxSize: 0, //只是提示,可以被违反,会尽量将chunk分的比maxSize小,当设为0代表能分则分,分不了不会强制
      minChunks: 1, //某个模块至少被多少代码块引用,才会被提取成新的chunk
      maxAsyncRequests: 5, //分割后,按需加载的代码块最多允许的并行请求数,在webpack5里默认值变为6
      maxInitialRequests: 3, //分割后,入口代码块最多允许的并行请求数,在webpack5里默认值变为4
      automaticNameDelimiter: "~", //代码块命名分割符
      name: true, //每个缓存组打包得到的代码块的名称
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, //匹配node_modules中的模块
          priority: -10, //优先级,当模块同时命中多个缓存组的规则时,分配到优先级高的缓存组
        },
        default: {
          minChunks: 2, //覆盖外层的全局属性
          priority: -20,
          reuseExistingChunk: true, //是否复用已经从原代码块中分割出来的模块
        },
      },
    },
  },
};

常用的vite插件 rollupPluginvisualizer:用于分析打包后的体积

vitePluginRestart:通过监听文件修改(如vite.config.ts,.env这些文件),自动重启vite服务,

vitePluginCompression:使用 gzip 或者 brotli 来压缩资源

写一个vite的plugin

export default function myPlugin() {
  return {
    name: 'transform-file-action',
    apply: 'build', //
    enforce: 'post', //强制修改执行顺序 post|pre
    //生命周期钩子,其他如config,configResolved,generateBundle,closeBundle等等
    transform(src, id) {
      if (fileRegex.test(id)) {
        return {
          code: compileFileToJS(src),
          map: null // 如果可行将提供 source map
        }
      }
    }
  }
}


手写一个loader并尝试运行成功

function loader(source){
return cb(source)
}
module.exports = loader;

手写一个plugin并运行成功

class Plugin{
constructor()
apply(compiler){
compiler.hooks.done.tap('name', ()=>{})
}
}

loader与plugin的区别

loader本质是一个函数,用来对函数接收到的内容做一个转换,运行在打包文件前 plugin是一个插件,或者说一个类,他接收compiler并提供一个apply方法,在apply中具体实现功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件并在功能中调用各生命周期的钩子如emit,run等等。

esbuild与rollup的区别

esbuild: go语言编写,先转换成esm rollup: 优秀的treeShaking,更适合各种库的打包

工作内容相关

了解jsBridge

image.png

webview-》native

暴露一个方法到window,即传入到webview,让native直接调用

evaluateJavascript

native-》webview

向Webview中注入JS API

通过webview提供的接口,将方法(一般同名)注入到window中,

window.NativeBridge.callAndroid('同名方法');

scheme规范(协议拦截)

如规定某种url形式

jsbridge://showToast?text=hello

Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,从来拦截Web发起的请求,我们对请求的格式进行判断:

  • 如果符合我们自定义的URL Schema,对URL进行解析,获取对应的方法和参数名,进而调用原生Native的方法
  • 如果不符合我们自定义的URL Schema,我们直接转发,请求真正的服务

通过iframe.src发起一个请求,客户端webview能拦截这个请求,做相应的处理。

a标签和location.href都可以做到,但是a灵活性欠缺,需要交互动作触发;location.href连续调用时,后一个请求会覆盖前一个。

尝试实现一个jsBridge

navigator.userAgent

juejin.cn/post/684490…

深入了解埋点

深入了解微前端

微前端的核心价值

  1. 确保时间跨度长的项目能够平滑的迁移,给遗产项目续命,能使用当前最新的技术栈
  2. 核心价值在于“技术栈无关”
  3. 解构巨石应用, 让每个子工程能够独立开发与部署

为什么是微前端不是iframe

1.iframe的dom结构是不共享的 2.通信困难

实现一份最简单的spa微前端

深入了解sentry

sentry问题上报原理

sentry重写了window.onerror方法捕获同步错误,window.onunhandledrejection方法捕获异步错误,采集用户信息,错误信息,版本信息,设备信息等等。

sentry性能上报原理

通过window.performance以及PerformanceObserver去观测性能

手动接入一个sentry

CI与CD

持续集成,持续交付,持续部署。 ci:lint检查代码是否规范,自动化运行测试查看代码能否通过测试。 CD 指的是在我们 CI 流程通过之后,将代码自动发布到服务器的过程。

深入了解接口加密 (对称与非对称加密)

Node相关

尝试自己写一个最简单的sdk

尝试自己写一个最简单的npm包

尝试自己实现一个接口并成功拿到数据(4)

JS相关

Promise面试题一次掌握

juejin.cn/post/684490…

Promise与async实现

juejin.cn/post/684490…

链式调用原理