胖蔡说技术
随便扯扯

使用Parcel作为React应用程序的打包工具

胖蔡阅读(102)

您可能已经熟悉用于项目资产管理的webpack。然而,还有另一个很酷的工具叫Parcel,它与webpack相当,因为它有助于轻松捆绑资产。Parcel真正闪光的地方在于,它不需要任何配置就可以启动和运行,而其他捆绑器通常需要编写大量代码才能启动。此外,Parcel在运行时速度极快,因为它利用了多核处理,而其他人则处理复杂而繁重的转换。

因此,简而言之,我们正在研究一些功能和优点:

  • 使用动态导入进行代码拆分
  • 任何类型文件的资产处理,当然也包括HTML、CSS和JavaScript
  • 热模块更换,在开发过程中无需刷新页面即可更新元素
  • 代码中的错误在记录时会突出显示,便于查找和更正
  • 易于区分本地开发和生产开发的环境变量
  • 通过防止不必要的构建步骤来加快构建的“生产模式”

希望您开始看到使用Parcel的充分理由。这并不是说它应该100%或一直使用,而是说在一些好的情况下它很有意义。

在本文中,我们将看到如何使用Parcel来设置React项目。在我们进行这项工作的同时,我们还将查看Create React App的替代方案,我们可以将其与Parcel一起用于开发React应用程序。这里的目标是以Parcel为例,了解React中还有其他工作方法。

创建一个新的工程

好的,我们首先需要的是一个项目文件夹,以便在本地工作。我们可以创建一个新文件夹,并直接从命令行导航到该文件夹:

$ mkdir csstricks-react-parcel && $_

接下来,让我们在那里获得我们必须的package.json文件。我们可以通过运行以下操作之一来使用npmYarn

## Using npm
$ npm init -y

## Using Yarn, which we'll continue with throughout the article
$ yarn init -y

这在我们的项目文件夹中为我们提供了一个package.json文件,其中包含我们在本地工作所需的默认配置。说到这一点,包包可以全局安装,但在本教程中,我们将作为开发依赖项在本地安装它。

我们在React工作时需要Babel,所以让我们开始吧:

$ yarn add parcel-bundler babel-preset-env babel-preset-react --dev

接下来,我们需要安装开发框架React、ReactDOM:

$ yarn add react react-dom

然后需要配置我们的babel配置文件:babel.config.js

{
  "presets": ["env", "react"]
}

接下来,我们在一个新的index.js文件中创建我们的基础应用程序组件。这里有一个简单返回“Hello”标题的快速方法:

import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
  render() {
    return (
      <React.Fragment>
        <h2>Hello!</h2>
      </React.Fragment>
    )
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

我们需要一个HTML文件来安装应用程序组件,所以让我们在src目录中创建一个index.HTML文件。同样,这里有一个非常简单的shell

<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Parcel React Example</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="./index.js"></script>
  </body>
</html>

我们将使用我们之前安装的Parcel软件包。为了实现这一点,我们需要编辑package.json文件中的启动脚本,使其看起来如下:

"scripts": {
  "start": "NODE_ENV=development parcel src/index.html --open"
}

最后,让我们回到命令行,运行yarn start。应用程序应该启动并打开一个指向的新浏览器窗口http://localhost:1234/.

使用样式

Parcel开箱即用PostCSS发货,但如果我们想使用其他东西,我们完全可以做到。例如,我们可以在项目中安装node-sass来使用sass

$ yarn add --dev node-sass autoprefixer

我们已经有了autoprefixer,因为它是一个PostCSS插件,所以我们可以在package.jsonPostCSS块中进行配置:

// ...
"postcss": {
  "modules": false,
  "plugins": {
    "autoprefixer": {
      "browsers": [">1%", "last 4 versions", "Firefox ESR", "not ie < 9"],
      "flexbox": "no-2009"
    }
  }
}

设置生产环境

我们希望确保我们的代码和资产是为生产使用而编译的,所以让我们确保告诉我们的构建过程这些代码和资产将去哪里。同样,在package-json中:

"scripts": {
  "start": "NODE_ENV=development parcel src/index.html --open",
  "build": "NODE_ENV=production parcel build dist/index.html --public-url ./"
}

运行yarn run构建现在将构建用于生产的应用程序,并将其输出到dist文件夹中。如果我们愿意,我们可以添加一些额外的选项来进一步完善:

  • --out-dir 目录名:这是为了使用生产文件的另一个目录,而不是默认的dist目录。
  • –no-minify:缩小是默认启用的,但我们可以使用此命令禁用。
  • –no-minify:这允许我们禁用文件系统缓存。

CRAP (Create React App Parcel)

Create React App Parcel(CRAP)是由Shawn Swyz Wang构建的一个包,用于帮助快速为Parcel设置React应用程序。根据文档,我们可以通过运行以下程序来引导任何应用程序:

$ npx create-react-app-parcel my-app

这将创建我们开始工作所需的文件和目录。然后,我们可以迁移到应用程序文件夹并启动服务器。

$ cd my-app
$ yarn start

Parcel 已全部设置完全

Parcel值得在您的下一个React应用程序中进行探索。事实上,没有必要的配置,捆绑包时间是超级优化的,这使得Parcel值得在未来的项目中考虑。而且,GitHub上有30000多颗星,看起来它在社区中越来越受欢迎。

其他

  • Parcel Examples::使用各种工具和框架封装示例。
  • Awesome Parcel:一份精心策划的清单,里面有很棒的Parcel资源、库、工具和样板。

bpmn-js 事件总线处理

胖蔡阅读(150)

bpmn-js 阅读指南:

bpmn-js中使用EventBus作为事件的处理句柄,EventBus的使用和我们常规使用的事件总线没啥大的区别,其源码位于:/diagram-js/lib/core/EventBus.js bpmn-js使用diagram-js实现流程图的web端绘制呈现工具)。

EventBus使用

事件总线使用基本都是以监听、回调的方式来实现的。diagram-js提供的EventBus也不无例外。如下为EventBus使用方式。

1、添加监听事件

diagram-js提供的EventBus在监听方式上提供了几种不同的选择,如下,可根据需要选择不同的监听方式:

  • 普通监听
  • 带参监听
  • 返回值监听
  • 优先级监听
  • 上下文监听
 * // listen for event
 * eventBus.on('foo', function(event) {
 *
 *   // access event type
 *   event.type; // 'foo'
 *
 *   // stop propagation to other listeners
 *   event.stopPropagation();
 *
 *   // prevent event default
 *   event.preventDefault();
 * });
 *
 * // listen for event with custom payload
 * eventBus.on('bar', function(event, payload) {
 *   console.log(payload);
 * });
 *
 * // listen for event returning value
 * eventBus.on('foobar', function(event) {
 *
 *   // stop event propagation + prevent default
 *   return false;
 *
 *   // stop event propagation + return custom result
 *   return {
 *     complex: 'listening result'
 *   };
 * });
 *
 *
 * // listen with custom priority (default=1000, higher is better)
 * eventBus.on('priorityfoo', 1500, function(event) {
 *   console.log('invoked first!');
 * });
 *
 *
 * // listen for event and pass the context (`this`)
 * eventBus.on('foobar', function(event) {
 *   this.foo();
 * }, this);

2、发送事件

EventBus通过fire来发送事件,发送事件可配合上述不同类型监听实现。

 *
 * // false indicates that the default action
 * // was prevented by listeners
 * if (eventBus.fire('foo') === false) {
 *   console.log('default has been prevented!');
 * };
 *
 *
 * // custom args + return value listener
 * eventBus.on('sum', function(event, a, b) {
 *   return a + b;
 * });
 *
 * // you can pass custom arguments + retrieve result values.
 * var sum = eventBus.fire('sum', 1, 2);
 * console.log(sum); // 3

3、其他操作

  • off:移除监听回调,若回调函数为空,则移除该监听的所有回调
  • createEvent: 创建一个可被EventBus识别的事件
  • once:注册一个只能被监听一次的事件

如何使用eventbus?

我们可以通过bpmn-js获取的viewer/modeler对象在diagram-js加载完成后添加事件监听。通过bpmn-js提供的eventbus来进行事件监听可以帮助我们给流程编辑器添加钩子和流程图交互配置,如通过监听事件适当添加配置属性。

1、使用viewer进行监听

viewer可以在加载完成diagram-js加载完成后通过viewer.get('eventBus')获取eventbus

var viewer = new BpmnJS({ container: bpmnContainer});

try {
    await viewer.importXML(diagramXM); // 此处异步操作完成后可进行事件操作

    var eventBus = viewer.get('eventBus');

    // you may hook into any of the following events
    var events = [
      'element.hover',
      'element.out',
      'element.click',
      'element.dblclick',
      'element.mousedown',
      'element.mouseup'
    ];

    events.forEach(function(event) {

  
      eventBus.on(event, function(e) {
        // e.element = the model element
        // e.gfx = the graphical element

        log(event, 'on', e.element.id);
      });
   });


} catch (err) {
	console.error('Error happened: ', err);
}

可以通过off来取消监听,但需要改变下写法:

var viewer = new BpmnJS({ container: bpmnContainer});

try {
    await viewer.importXML(diagramXM); // 此处异步操作完成后可进行事件操作

    var eventBus = viewer.get('eventBus');

    function ensureHoveringProcess(event) {
      event.element = rootElement;
      event.gfx = rootElementGfx;
    }
  eventBus.on('element.hover', ensureHoveringProcess)
  // 监听之后
   eventBus.off('element.hover', ensureHoveringProcess);
// 或者如下取消所有element.hover的监听
  eventBus.off('element.hover')


} catch (err) {
	console.error('Error happened: ', err);
}

2、使用Modeler进行监听

modeler可以直接使用modeler对象进行监听和取消监听操作,而无需额外获取:

modeler.on('commandStack.changed', () => {
  // user modeled something or
  // performed an undo/redo operation
});

modeler.on('element.changed', (event) => {
  const element = event.element;

  // the element was changed by the user
});

3、依赖注入

bpmn-js提供给我们足够大的自定义空间,通过在modeler/viewer中的additionalModules配置让我们可以进行各类插件的自定义改装操作,可参照Bpmn-js自定义Palette

const bpmnModeler = new BpmnModeler({
        container: this.$refs["bpmn-canvas"],
        additionalModules: [],

});

而自定义的additionalModules通过使用$inject属性来声明依赖注入的各个模块。如此我们也可以通过这种方式创建一个单独进行logger记录的插件:

 // logger插件
  function InteractionLogger(eventBus) {
    eventBus.on('element.hover', function(event) {
      console.log()
   })
 }
 
 InteractionLogger.$inject = [ 'eventBus' ]; // 注入插件模块
 
  // 插件模块声明
 var extensionModule = {
   __init__: [ 'interactionLogger' ],
  interactionLogger: [ 'type', InteractionLogger ]
};

// viewer加载
var bpmnViewer = new Viewer({
  container:viewerContainer, 
  additionalModules: [ extensionModule ] 
});
// modeler加载
var bpmnModeler = new BpmnModeler({
  container:viewerContainer, 
  additionalModules: [ extensionModule ] 
})

我们也可以通过自定义元素shape、palette时注入eventbus,添加我们自己的事件监听。

内置事件

通过diagram-js实现的元素绘制、布局相应的其会在内部内置元素的各类事件以提供我们调试,跟踪事件以及其他额外元素操作使用,在使用bpmn-js较为常见的事件监听如下:

1、元素事件类

element.changed,element.out,element.hover,element.updateId,element.marker.update,bpmnElement.added

2、copyPaste类

moddleCopy.canCopyProperties,moddleCopy.canSetCopiedProperty,copyPaste.canCopyElements,
copyPaste.elementsCopied,copyPaste.pasteElements,copyPaste.pasteElement,copyPaste.createTree,copyPaste.copyElement

3、contextPad类

contextPad.trigger,contextPad.open,contextPad.create,contextPad.close

4、render类

canvas.viewbox.changing,canvas.init,canvas.viewbox.changed,canvas.resized,render.shape,render.getShapePath,render.connection,render.getConnectionPath,canvas.destroy,diagram.init,diagram.destroy,diagram.clear

5、connect类

connection.added,connection.removed,connect.ended,connect.canceled

【一周一荐】 | vite-plugin-pwa 离线安装Vite应用

胖蔡阅读(165)

渐进式Web应用(PWA)通过结合 Web 和移动应用的特点,为用户带来更加流畅和快速的体验。且PWA支持离线访问能力(访问静态资源本地缓存),极大提高了用户交互的流畅性,降低非必要的网络依赖。尤其适合在手机端创建,本文推荐基于Vite的基础上使用vite-plugin-pwa实现A2HS(Add To Home Screen)workbox离线缓存功能。

插件安装

$ npm i vite-plugin-pwa -D

配置

1、添加VitePWA插件

安装完成vite-plugin-pwa插件后,我们只需要在vite.config.ts文件中将其引入就完成基础的pwa配置了。

// vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { VitePWA } from 'vite-plugin-pwa'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VitePWA({
        registerType:"autoUpdate",
        devOptions: {
            enable: true,
        }
    }), // 添加vite-plugin-pwa插件支持
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

我当前安装的vite-plugin-pwa版本为0.18.1,具体支持的插件配置请参考对照版本。其比较重要的支持配置项如下:

  • mode: 开发环境
  • manifest/manifestFilenamepwa对应的应用配置
  • strategies:默认是generateSW然后去配置workbox; 如果想要更多自定义的设置,可以选择injectManifest,那就对应配置injectManifest
  • workbox:给generateSW的配置,配置的所有workbox,将交给workbox-build插件中的generateSW处理,生成最终sw.js中的配置代码
  • registerType:注册类型配置,用于指定 PWA 的注册方式。这里设置为 'autoUpdate',表示自动更新注册方式。

如下是VitePWAOptions 支持的所有配置项:

/**
 * Plugin options.
 */
interface VitePWAOptions {
   
    mode?: 'development' | 'production';
    srcDir?: string; // 默认public
    outDir?: string; // 默认dist
    filename?: string; // 默认sw.js
    manifestFilename?: string; // 默认 manifest.webmanifest
    strategies?: 'generateSW' | 'injectManifest'; // 默认 generateSW
    scope?: string; // 注册 service worker范围
    injectRegister: 'inline' | 'script' | 'script-defer' | 'auto' | null | false; // 默认auto
    registerType?: 'prompt' | 'autoUpdate'; // 默认 prompt
    minify: boolean; // 默认 true
    manifest: Partial<ManifestOptions> | false; // manifest配置对象
    useCredentials?: boolean; // 是否添加crossorigin="use-credentials"到<link rel="manifest">,默认false
    workbox: Partial<GenerateSWOptions>; // google workbox配置对象,
    injectManifest: Partial<CustomInjectManifestOptions>;
    base?: string; // 覆盖vite的base配置,仅仅对于pwa
    includeAssets: string | string[] | undefined;
    includeManifestIcons: boolean;
    disable: boolean; // 是否在“生成”上禁用service worker注册和生成?默认false
    integration?: PWAIntegration; // Vite PWA集成
    devOptions?: DevOptions; // 开发环境配置
    selfDestroying?: boolean; // 是否销毁service worker,默认false
    buildBase?: string; // 构建配置,默认使用vite.config.ts中的配置
}

其支持的

2、添加mainfest配置

manifest用于配置pwa应用的基础信息:如名称、图标、主题色等,也可以选择创建manifest文件来配置应用信息:

  • name : 应用名
  • icons: 应用图标
  • description:应用描述信息
  • short_name: 应用简称
  • theme_color: 样式主题色,默认#42b883
  • background_color:背景色,默认#fff
  • lang:语言,默认en
  • shortcuts:快捷方式的配置信息

import { defineConfig } from 'vite'
...
import { VitePWA } from 'vite-plugin-pwa'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
......

    VitePWA({
      manifest: {
        name: 'Vite PWA App',
        short_name: 'v-pwa',
        description: '一个Vite PWA测试APP',
        theme_color: '#fafafa',
        icons: [
          {
            src: '/icons/icon-192x192.png',
            sizes: '192x192',
            type:'image/png',
          },
          {
            src: '/icons/icon-512x512.png',
            sizes: '512x512',
            type: 'image/png',
          }
        ],
        shortcuts: [ // 配置快捷方式,指定打开页面地址
          {
            name: "打开首页", // 快捷方式名称
            short_name: "首页", // 快捷方式简称
            description: "打开首页", // 快捷方式描述
            url: "/", // 快捷方式链接地址
            icons: [{ src: "/favicon.ico", sizes: "36x36" }], // 快捷方式图标配置
          },
        ]
      },
    })
  ],
......
})

3、配置workbox

workbox用于帮助我们处理资源的缓存更新,我们只需要配置一个workbox配置即可按照规则对资源进行本地缓存、以及运行时缓存等操作。

import { defineConfig } from 'vite'
...
import { VitePWA } from 'vite-plugin-pwa'

const getCache = ({ name, pattern }: any) => ({
  urlPattern: pattern,
  handler: 'CacheFirst' as const,
  options: {
    cacheName: name,
    expiration: {
      maxEntries: 500,
      maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years
    },
    cacheableResponse: {
      statuses: [200]
    }
  }
})

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
......

    VitePWA({
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,jpg,svg}'], //缓存相关静态资源
        runtimeCaching: [
          // 配置自定义运行时缓存
          getCache({
            pattern: /^https:\/\/enjoytoday.cn\/wp-uploads/, 
            name: 'local-upload'
          }),
          getCache({
            pattern: /^https:\/\/app.enjoytoday.cn/,
            name: 'webapp'
          })
        ]
      }
    })
  ],
......
})

4、完整配置

如下,给出有关VitePWA插件在vite.config.ts中的完成配置信息。

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { VitePWA } from 'vite-plugin-pwa'

const getCache = ({ name, pattern }: any) => ({
  urlPattern: pattern,
  handler: 'CacheFirst' as const,
  options: {
    cacheName: name,
    expiration: {
      maxEntries: 500,
      maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years
    },
    cacheableResponse: {
      statuses: [200]
    }
  }
})

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VitePWA({
      manifest: {
        name: 'Vite PWA App',
        short_name: 'v-pwa',
        description: '一个Vite PWA测试APP',
        theme_color: '#fafafa',
        icons: [
          {
            src: '/icons/icon.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/icons/icon.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ],
        shortcuts: [
          {
            name: '打开首页', // 快捷方式名称
            short_name: '首页', // 快捷方式简称
            description: '打开首页', // 快捷方式描述
            url: '/', // 快捷方式链接地址
            icons: [{ src: '/favicon.ico', sizes: '36x36' }] // 快捷方式图标配置
          }
        ]
      },
      registerType: "autoUpdate", // 注册类型配置
      devOptions: {
        enabled: true, // 开发选项配置,启用插件
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,jpg,svg}'], //缓存相关静态资源
        runtimeCaching: [
          // 配置自定义运行时缓存
          getCache({
            pattern: /^https:\/\/enjoytoday.cn\/wp-uploads/, 
            name: 'local-upload'
          }),
          getCache({
            pattern: /^https:\/\/app.enjoytoday.cn/,
            name: 'webapp'
          })
        ]
      }
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

应用安装

通过上述配置我们添加了一个离线引用,当浏览器支持A2HS的情况下,我们可以通过代码对其进行一个安装并添加桌面上,以便于我们能过快速抵达应用。如下介绍如何安装应用。

1、配置应用安装触发

希望安装应用到桌面需要我们预先配置应用安装触发,首先需要在入口处添加监听,然后通过我们的交互方式进行应用的安装操作(或者通过工具栏的图标主动安装)。

// 在主入口监听PWA注册事件,如main.ts
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault()
  window.deferredPrompt = e
})

// 在具体页面添加安装,如App.vue
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'

// 若浏览器支持,则这里会出发安装操作
const openAddFlow = () => {
  try {
    window.deferredPrompt.prompt()
    window.deferredPrompt.userChoice.then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        // showAddToDesktop.value = false
        localStorage.setItem('addDesktop', true)
      } else {
        console.log('User dismissed the A2HS prompt')
      }
      window.deferredPrompt = null
    })
  } catch {
   //
  }
}
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125"  @click="openAddFlow"/>

    <div class="wrapper">
      <HelloWorld msg="You did it!"  />

      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>

2、应用安装

3、缓存

我并未将其部署线上地址,也没有添加配置的运行时请求数据,所以这里不会展示配置的运行时缓存信息。

我在开始第一个React项目时犯的5个错误

胖蔡阅读(96)

你知道学习一种新语言或框架的感觉。有时有很棒的文档可以帮助你找到方法。但即使是最好的文档也不能涵盖所有内容。当你处理新事物时,你一定会发现一个没有书面解决方案的问题。

这就是我第一次创建React项目时的情况——React是一个有着出色文档的框架,尤其是现在有了测试版文档。但我还是挣扎着度过了难关。这个项目已经过去很长一段时间了,但我从中得到的教训仍然历历在目。尽管有很多React“如何操作”教程,但我想我会分享我第一次使用它时希望知道的内容。

所以,这就是本文的内容——列出了我早期犯的错误。我希望它们能帮助你更顺利地学习React。

使用create-react应用程序启动项目

Create React App(CRA)是一个帮助您建立新React项目的工具。它为大多数React项目创建了一个具有最佳配置选项的开发环境。这意味着您不必花费时间自己配置任何内容。

作为一个初学者,这似乎是开始我工作的好方法!没有配置!开始编码!

CRA使用两个流行的包来实现这一点,即webpack和Babel。webpack是一个web捆绑器,它可以优化项目中的所有资产,如JavaScript、CSS和图像。Babel是一个允许您使用较新JavaScript功能的工具,即使某些浏览器不支持这些功能。

两者都很好,但有一些更新的工具可以做得更好,特别是Vite和Speedy Web编译器(SWC)。

这些新的和改进的替代方案比webpack和Babel更快、更容易配置。这使得在不弹出的情况下更容易调整配置,而在创建react应用程序时很难做到这一点。

要在设置新的React项目时同时使用它们,您必须确保安装了Node版本12或更高版本,然后运行以下命令。

npm create vite

您将被要求为您的项目选择一个名称。完成后,从框架列表中选择React。之后,您可以选择Javascript+SWC或Typescript+SWC

然后,您必须将目录cd更改为您的项目,并运行以下命令;

npm i && npm run dev

这应该为您的网站运行一个URL为localhost:5173的开发服务器

就这么简单。

使用defaultProps作为默认值

数据可以通过称为props的东西传递给React组件。它们就像HTML元素中的属性一样被添加到组件中,并且可以通过将传入的prop对象中的相关值作为参数来在组件的定义中使用。

// App.jsx
export default function App() {
  return <Card title="Hello" description="world" />
}

// Card.jsx
function Card(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.description}</p>
    </div>
  );
}

export default Card;

如果道具需要默认值,则可以使用defaultProp属性:

// Card.jsx
function Card(props) {
  // ...
}

Card.defaultProps = {
  title: 'Default title',
  description: 'Desc',
};

export default Card;

使用现代JavaScript,可以破坏props对象,并在函数参数中为其分配一个默认值。

// Card.jsx
function Card({title = "Default title", description= "Desc"}) {
  return (
    <div>
      <h1>{title}</h1>
      <p>{description}</p>
    </div>
  )
}

export default Card;

这是更有利的,因为现代浏览器可以读取代码,而不需要额外的转换。

不幸的是,defaultProp确实需要一些转换才能被浏览器读取,因为不支持开箱即用的JSX(JavaScriptXML)。这可能会影响使用大量defaultProp的应用程序的性能。

不要使用propTypes

在React中,propTypes属性可用于检查组件是否被传递了其props的正确数据类型。它们允许您指定应用于每个道具的数据类型,如字符串、数字、对象等。它们还允许您指定是否需要道具。

这样,如果组件被传递了错误的数据类型,或者没有提供所需的道具,那么React将抛出一个错误。

// Card.jsx
import { PropTypes } from "prop-types";

function Card(props) {
  // ...
}

Card.propTypes = {
  title: PropTypes.string.isRequired,
  description: PropTypes.string,
};

export default Card;

TypeScript在传递给组件的数据中提供了一个类型安全级别。所以,当然,当我刚开始的时候,propTypes是个好主意。然而,既然TypeScript已经成为类型安全的首选解决方案,我强烈建议使用它而不是其他任何东西。

// Card.tsx
interface CardProps {
  title: string,
  description?: string,
}

export default function Card(props: CardProps) {
  // ...
}

TypeScript是一种通过添加静态类型检查在JavaScript之上构建的编程语言。TypeScript提供了一个更强大的类型系统,可以捕捉更多潜在的bug并改善开发体验。

使用Class组件

React中的类组件是使用JavaScript类创建的。它们有一个更面向对象的结构,还有一些额外的功能,比如使用this关键字和生命周期方法的能力。

// Card.jsx
class Card extends React.Component {
  render() {
    return (
      <div>
        <h1>{this.props.title}</h1>
        <p>{this.props.description}</p>
      </div>
    )
  }
}

export default Card;

比起函数,我更喜欢用类来编写组件,但JavaScript类对初学者来说更难理解,这可能会让人非常困惑。相反,我建议将组件编写为函数:

// Card.jsx
function Card(props) {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.description}</p>
    </div>
  )
}

export default Card;

函数组件只是返回JSX的JavaScript函数。它们更容易阅读,并且并没有像this关键字和生命周期方法这样的附加功能,这使它们比类组件更具性能。

功能组件还具有使用钩子的优点。React Hook允许您在不编写类组件的情况下使用状态和其他React特性,使您的代码更具可读性、可维护性和可重用性。

不必要地导入React

由于React 17于2020年发布,现在无论何时创建组件,都没有必要在文件顶部导入React。

import React from 'react'; // Not needed!
export default function Card() {}

但我们必须在React 17之前这样做,因为JSX转换器(将JSX转换为常规JavaScript的东西)使用了一个名为React.createElement的方法,该方法仅在导入React时有效。从那时起,一个新的转换器已经发布,它可以在不使用createElement方法的情况下转换JSX。

您仍然需要导入React以使用钩子、片段以及库中可能需要的任何其他函数或组件:

import { useState } from 'react';

export default function Card() {
  const [count, setCount] = useState(0);
  // ...
}

这些都是我早期的错误!

也许“错误”这个词太苛刻了,因为后来出现了一些更好的做法。尽管如此,我还是看到了很多“旧”做事方式仍在项目和其他教程中被积极使用的例子。

老实说,我刚开始的时候可能犯了五个以上的错误。任何时候你接触到一个新工具,都更像是一次有效使用它的学习之旅,而不是切换开关。但这些都是我多年后仍然随身携带的东西!

如果你已经使用React一段时间了,你希望在开始之前知道哪些事情?如果能推出一个系列来帮助其他人避免同样的困难,那就太好了。

使用VitePWA插件使网站离线工作

胖蔡阅读(105)

Anthony Fu的VitePWA插件是您的Vite网站的绝佳工具。它可以帮助您添加一个服务人员来处理:

  • 离线支持
  • 缓存资产和内容
  • 当有新内容可用时提示用户

…还有其他好东西!

我们将一起探讨服务工作者的概念,然后直接用VitePWA p制作一个

Service workers, 介绍

在进入VitePWA插件之前,让我们简单地谈谈Service Worker本身。

服务工作者是在web应用程序中的独立线程上运行的后台进程。服务人员有能力拦截网络请求并做任何事情。可能性之大令人惊讶。例如,您可以拦截对TypeScript文件的请求并实时编译它们。或者,您可以拦截对视频文件的请求,并执行浏览器当前不支持的高级转码。不过,更常见的情况是,服务工作者用于缓存资产,既可以提高网站的性能,也可以使其在离线时做一些事情。

当有人第一次登录您的网站时,VitePWA插件创建的服务人员将安装并通过利用Cache Storage API缓存您的所有HTML、CSS和JavaScript文件。结果是,在随后访问您的网站时,浏览器将从缓存中加载这些资源,而不需要发出网络请求。即使是在第一次访问您的网站时,由于服务人员刚刚预缓存了所有内容,您的用户单击的下一个位置可能已经预缓存,从而允许浏览器完全绕过网络请求。

Versioning 和manifests

您可能想知道当您的代码更新时,服务人员会发生什么。如果你的服务人员正在缓存一个foo.js文件,并且你修改了该文件,你希望服务人员在下次用户访问该网站时下拉更新的版本。

但在实践中,您没有foo.js文件。通常,构建系统会创建类似foo-ABC123.js的东西,其中“ABC123”是文件的散列。如果你更新了foo.js,你的站点的下一次部署可能会通过foo-XYZ987.js发送。服务人员是如何处理的?

结果发现服务工作者API是一个非常低级的原语。如果您正在寻找它和缓存API之间的本地交钥匙解决方案,您会很失望。基本上,服务工作者的创建在一定程度上需要自动化,并连接到构建系统。您需要查看构建创建的所有资产,将这些文件名硬编码到服务工作者中,并有代码预缓存它们,更重要的是,跟踪缓存的文件。

如果代码更新,服务工作者文件也会发生更改,其中包含新的文件名和散列。当用户下次访问该应用程序时,新的服务工作者将需要安装新的文件清单,并将其与当前缓存中的清单进行比较,在缓存新内容的同时弹出不再需要的文件。

这是一个荒谬的工作量和难以置信的难以做到正确。虽然这可能是一个有趣的项目,但在实践中,你会想使用一个成熟的产品来培养你的服务人员——最好的产品是Workbox,它来自谷歌的员工。

甚至Workbox也是一个低级的原语。它需要有关预缓存文件的详细信息,这些文件隐藏在构建工具中。这就是我们使用VitePWA插件的原因。它在后台使用Workbox,并使用Vite创建的捆绑包的所有信息对其进行配置。毫不奇怪,如果你碰巧喜欢使用这些捆绑器,也有webpack和Rollup插件。

我们的第一个service worker

我假设你已经有了一个基于Vite的网站。如果没有,请随时从任何可用的模板中创建一个模板。

首先,我们安装VitePWA插件:

npm i vite-plugin-pwa

我们将在Vite配置中导入插件:

import { VitePWA } from "vite-plugin-pwa"

然后我们也将其用于配置中:

plugins: [
  VitePWA()

我们将添加更多选项,但这就是我们创建一个非常有用的服务工作者所需要的全部内容。现在,让我们用以下代码在应用程序的条目中的某个位置注册它:

import { registerSW } from "virtual:pwa-register";

if ("serviceWorker" in navigator) {
  // && !/localhost/.test(window.location)) {
  registerSW();
}

不要让注释掉的代码让你陷入循环。事实上,这非常重要,因为它阻止了服务工作者在开发中运行。我们只想在不在我们正在开发的本地主机上的任何地方安装服务工作者,也就是说,除非我们正在开发服务工作者本身,在这种情况下,我们可以注释掉该检查(并在将代码推送到主分支之前恢复)。

让我们继续打开一个新的浏览器,启动DevTools,导航到“网络”选项卡,然后运行web应用程序。所有的东西都应该像你通常预期的那样加载。不同的是,您应该在DevTools中看到大量的网络请求。

这是Workbox预缓存捆绑包。事情进展顺利!

脱机功能如何?

因此,我们的服务人员正在预缓存所有捆绑资产。这意味着它将从缓存中为这些资产提供服务,甚至不需要访问网络。这是否意味着即使用户没有网络访问权限,我们的服务人员也可以为资产提供服务?的确如此!

信不信由你,它已经完成了。尝试一下,打开DevTools中的“网络”选项卡,告诉Chrome模拟离线模式,就像这样。

让我们刷新页面。你应该看到所有的负载。当然,如果你正在运行任何网络请求,你会看到它们在离线后永远挂起。即使在这里,你也可以做一些事情。现代浏览器自带自己的内部持久数据库IndexedDB。没有什么可以阻止您编写自己的代码来将一些数据同步到那里,然后编写一些自定义的服务工作者代码来拦截网络请求,确定用户是否离线,然后在IndexedDB中提供等效内容(如果它在那里的话)。

但一个简单得多的选项是检测用户是否离线,显示关于离线的消息,然后绕过数据请求。这是一个独立的主题,我已经写了更详细的内容。

在向您展示如何编写和集成您自己的服务工作者内容之前,让我们仔细看看我们现有的服务工作者。特别是,让我们看看它是如何管理更新/更改内容的。即使使用VitePWA插件,这也非常棘手且容易出错。

在继续之前,一定要告诉Chrome DevTools让你重新上线。

如何更新?

仔细看看当我们更改内容时,我们的网站会发生什么。我们将继续删除现有的服务人员,这可以在DevTools的“应用程序”选项卡的“存储”下进行。

单击“清除站点数据”按钮以获得全新的记录。当我在做的时候,我会删除我自己网站的大部分路线,这样资源就会更少,然后让Vite重建应用程序。

在生成的sw.js中查看生成的Workbox服务工作者。里面应该有一个预缓存清单。我的清单看起来是这样的:

现在让我们运行该网站,看看我们的缓存中有什么:

让我们关注settings.js文件。Vite根据其内容的哈希生成了assets/settings.cb0800c2.js。Workbox独立于Vite,生成了相同文件的自己的哈希。如果相同的文件名是用不同的内容生成的,那么将重新生成一个新的服务工作者,使用不同的预缓存清单(相同的文件,但不同的修订版),Workbox将知道缓存新版本,并在不再需要时删除旧版本。

同样,文件名总是不同的,因为我们使用的是将哈希代码注入文件名的bundler,但Workbox支持不这样做的开发环境。

如果我们更新settings.js文件,那么Vite将在我们的构建中创建一个新文件,其中包含一个新的哈希代码,Workbox将其视为新文件。让我们看看这一点的实际效果。在更改文件并重新运行Vite构建后,我们的预缓存清单如下所示:

现在,当我们刷新页面时,先前的服务工作者仍在运行并加载先前的文件。然后,下载并预缓存带有新预缓存清单的新服务工作者。

新的预缓存清单显示在缓存资产列表中。请注意,我们的设置文件的两个版本都在那里(其他一些资产的两种版本也受到了影响):旧版本,因为它仍在运行,而新版本,因为新的服务工作者已经预缓存了它。

注意这里的推论:由于旧的服务工作者仍在运行,我们的旧内容仍在提供给用户。用户无法看到我们刚刚做出的更改,即使它们刷新了,因为默认情况下,服务人员会保证此web应用程序的任何和所有选项卡都运行相同的版本。如果您希望浏览器显示更新的版本,请关闭您的选项卡(以及网站的任何其他选项卡),然后重新打开。

Workbox做了所有的跑腿工作,使这一切顺利进行!我们几乎没有采取什么措施来推动这一进程。

一个更好的思路

在用户关闭所有浏览器选项卡之前,你不太可能为用户提供过时的内容。幸运的是,VitePWA插件提供了一种更好的方式。registerSW函数接受具有onNeedRefresh方法的对象。每当有新的服务工作者等待接管时,就会调用此方法。registerSW还返回一个函数,您可以调用该函数来重新加载页面,从而激活流程中的新服务工作者。

这太多了,所以让我们看看一些代码:

if ("serviceWorker" in navigator) {
  // && !/localhost/.test(window.location) && !/lvh.me/.test(window.location)) {
  const updateSW = registerSW({
    onNeedRefresh() {
      Toastify({
        text: `<h4 style='display: inline'>An update is available!</h4>
               <br><br>
               <a class='do-sw-update'>Click to update and reload</a>  `,
        escapeMarkup: false,
        gravity: "bottom",
        onClick() {
          updateSW(true);
        }
      }).showToast();
    }
  });
}

我使用toast-ify js库来显示一个toast UI组件,让用户知道何时有新版本的服务工作者可用并等待。如果用户单击toast,我将调用VitePWA提供的函数来重新加载页面,并运行新的服务工作者。

这里需要记住的一件事是,在部署代码以显示toast之后,下次加载网站时,toast组件将不会显示。这是因为旧的服务工作者(在我们添加toast组件之前的那个)仍在运行。这需要手动关闭所有选项卡,然后重新打开web应用程序,由新的服务人员接管。然后,下次更新某些代码时,服务人员应该显示toast,提示您进行更新。

为什么服务工作者在刷新页面时不更新?我之前提到过刷新页面不会更新或激活等待的服务人员,那么为什么这样做有效呢?调用此方法不仅可以刷新页面,还可以调用一些低级的Service Worker API(特别是skipWaiting),为我们提供所需的结果。

运行时缓存

我们已经看到了VitePWA为我们的构建资产免费提供的捆绑包预缓存。在运行时缓存我们可能请求的任何其他内容怎么样?Workbox通过其runtimeCaching功能支持这一点。

方法如下。VitePWA插件可以接受一个对象,其中一个属性是workbox,它接受workbox属性。

const getCache = ({ name, pattern }: any) => ({
  urlPattern: pattern,
  handler: "CacheFirst" as const,
  options: {
    cacheName: name,
    expiration: {
      maxEntries: 500,
      maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years
    },
    cacheableResponse: {
      statuses: [200]
    }
  }
});
// ...

  plugins: [
    VitePWA({
      workbox: {
        runtimeCaching: [
          getCache({ 
            pattern: /^https:\/\/s3.amazonaws.com\/my-library-cover-uploads/, 
            name: "local-images1" 
          }),
          getCache({ 
            pattern: /^https:\/\/my-library-cover-uploads.s3.amazonaws.com/, 
            name: "local-images2" 
          })
        ]
      }
    })
  ],
// ...

我知道,这是很多代码。但它真正做的只是告诉Workbox缓存它看到的与这些URL模式匹配的任何内容。如果你想深入了解细节,这些文档会提供更多信息。

现在,在更新生效后,我们可以看到这些资源由我们的服务人员提供服务。

我们可以看到创建的相应缓存。

添加你自己的Service worker内容

比方说,你想和你的服务人员一起进步。您想添加一些代码来与IndexedDB同步数据,添加提取处理程序,并在用户离线时使用Indexed数据库数据进行响应(同样,我之前的文章介绍了IndexedDB的来龙去脉)。但是,您如何将自己的代码放入Vite为我们创建的服务人员中呢?

我们可以使用另一个Workbox选项:importScripts。

VitePWA({
  workbox: {
    importScripts: ["sw-code.js"],

在这里,服务工作者将在运行时请求sw-code.js。在这种情况下,请确保应用程序可以提供sw-code.js文件。实现这一点的最简单方法是将其放在公共文件夹中(有关详细说明,请参阅Vite文档)。

如果此文件开始增长到需要使用JavaScript导入进行分解的大小,请确保将其捆绑在一起,以防止您的服务工作者尝试执行导入语句(可能无法执行,也可能无法执行)。您可以创建一个单独的Vite构建。

总结

2021年底,CSS Tricks询问了一群前端人员,有一件事可以让他们的网站变得更好。Chris Ferdinandi建议找一位服务人员。好吧,这正是我们在这篇文章中完成的,它相对简单,不是吗?这要归功于向WorkboxCache API提供帽子提示的VitePWA

利用Cache API的服务人员能够极大地提高web应用程序的性能。虽然一开始可能看起来有点可怕或令人困惑,但很高兴知道我们有像VitePWA插件这样的工具可以大大简化事情。安装插件,让它完成繁重的工作。当然,服务人员可以做更高级的事情,VitePWA可以用于更复杂的功能,但离线网站是一个极好的起点!

将Vite添加到您现有的Web应用程序

胖蔡阅读(109)

Vite(发音为“veet”)是一个新的JavaScript绑定器。它包括电池,几乎不需要任何配置即可使用,并包括大量配置选项。哦——而且速度很快。速度快得令人难以置信。

本文将介绍将现有项目转换为Vite的过程。我们将介绍别名、填充webpack的dotenv处理和服务器代理等内容。换句话说,我们正在研究如何将一个项目从现有的bundler转移到Vite。如果你想开始一个新的项目,你会想跳转到他们的文档。

长话短说:CLI会询问您选择的框架——React、Preact、Svelte、Vue、Vanilla,甚至lit-html——以及您是否想要TypeScript,然后为您提供一个功能齐全的项目。

我们的用例

我们所看到的是基于我自己迁移booklist项目(repo)的webpack构建的经验。这个项目没有什么特别的地方,但它相当大,而且很旧,而且非常依赖webpack。因此,从这个意义上说,这是一个很好的机会,可以在我们迁移到Vite时看到一些更有用的配置选项。

我们不需要什么

使用Vite最令人信服的原因之一是,它已经做了很多开箱即用的工作,结合了其他框架的许多职责,因此配置和约定的依赖性更少,基线也更稳固。

所以,与其从喊出我们需要开始的内容开始,不如让我们回顾一下我们不需要的所有常见的网络包东西,因为Vite免费给了我们。

静态资产加载

我们通常需要在webpack中添加这样的内容:

{
  test: /\.(png|jpg|gif|svg|eot|woff|woff2|ttf)$/,
  use: [
    {
      loader: "file-loader"
    }
  ]
}

这将获取对字体文件、图像、SVG文件等的任何引用,并将它们复制到dist文件夹中,以便可以从新的捆绑包中引用它们。这是Vite的标准配置。

Styles

我在这里故意说“styles”而不是“css”,因为有了webpack,你可能会有这样的东西:

{
  test: /\.s?css$/,
  use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
},

// later

new MiniCssExtractPlugin({ filename: "[name]-[contenthash].css" }),

…这允许应用程序导入CSS或SCSS文件。你会厌倦听我这么说的,但Vite支持这一点。只要确保将Sass本身安装到您的项目中,Vite将处理其余的工作。

模板转换/TypeScript

很可能您的代码正在使用TypeScript和/或非标准JavaScript功能,如JSX。如果是这种情况,您将需要转换代码以删除这些内容,并生成浏览器(或JavaScript解析器)能够理解的普通旧JavaScript。在webpack中,它看起来像这样:

{
  test: /\.(t|j)sx?$/,
  exclude: /node_modules/,
  loader: "babel-loader"
},

…用相应的Babel配置来指定适当的插件,对我来说,它看起来是这样的:

{
  "presets": ["@babel/preset-typescript"],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-nullish-coalescing-operator"
  ]
}

虽然我可能在几年前就停止使用前两个插件了,但这并不重要,因为正如我相信你已经猜到的那样,Vite为我们做了这一切。它会获取你的代码,删除任何TypeScript和JSX,并生成现代浏览器支持的代码。

node_modules

令人惊讶的是,webpack要求您告诉它从node_modules解析导入,我们这样做:

resolve: {
  modules: [path.resolve("./node_modules")]
}

不出所料,Vite已经做到了。

生产模式

我们在webpack中做的一件常见的事情是通过手动传递模式属性来区分生产环境和开发环境,如下所示:

mode: isProd ? "production" : "development",

…我们通常会这样推测:

const isProd = process.env.NODE_ENV == "production";

当然,我们通过构建过程来设置环境变量。

Vite处理这一问题的方式有点不同,它为我们提供了不同的命令来运行开发构建和生产构建,我们很快就会了解这些命令。

文件扩展

冒着拖延这一点的风险,我会很快注意到Vite也不要求您指定您正在使用的每个文件扩展名。

resolve: {
  extensions: [".ts", ".tsx", ".js"],
}

只要建立一个合适的Vite项目,你就可以开始了。

支持 Rollup 插件

这是一个关键点,我想在它自己的部分中指出。如果你在完成这篇博客文章时,仍然需要在Vite应用程序中替换一些webpack插件,那么试着找到一个等效的Rollup插件并使用它。你没有看错:Rollup插件已经(或者通常,至少)与Vite兼容。当然,一些Rollup插件所做的事情与Vite的工作方式不兼容——但总的来说,它们应该只是工作的。

详情请查看: 使用插件

你的第一个Vite项目

请记住,我们正在将现有的遗留webpack项目转移到Vite。如果你正在建造新的东西,最好开始一个新的Vite项目,然后从那里开始。也就是说,我向您展示的初始代码基本上是从Vite从一个新项目中构建的代码复制而来的,所以花点时间构建一个新的项目可能也是比较流程的好主意。

HTML入口

是的,你没看错。Vite不是像webpack那样将HTML集成放在插件后面,而是将HTML放在首位。它期望在JavaScript入口点上有一个带有脚本标记的HTML文件,并从中生成所有内容。

以下是我们开始使用的HTML文件(Vite希望将其称为index.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>The GOAT of web apps</title>
  </head>
  <body>
    <div id="home"></div>
    <script type="module" src="/reactStartup.tsx"></script>
  </body>
</html>

请注意,<script>标记指向/reactStartup.tsx。根据需要将其调整为您自己的条目。

让我们安装一些东西,比如React插件:

$ npm i vite @vitejs/plugin-react @types/node

我们还在项目目录中的index.html文件旁边创建以下vite.config.ts。

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()]
});

最后,让我们添加一些新的npm脚本:

"dev": "vite",
"build": "vite build",
"preview": "vite preview",

现在,让我们用npm运行的dev启动Vite的开发服务器。它速度快得令人难以置信,可以根据请求逐步构建所需的内容。

但是,不幸的是,它失败了。至少现在是这样。

我们稍后将了解如何设置别名,但现在,让我们改为修改reactStartup文件(或您的入口文件的名称),如下所示:

import React from "react";
import { render } from "react-dom";

render(
  <div>
    <h1>Hi there</h1>
  </div>,
  document.getElementById("home")
);

现在我们可以通过npm run-dev命令运行它,并浏览到localhost:3000

HMR(热更新模块)

现在开发服务器正在运行,请尝试修改源代码。输出应该几乎立即通过Vite的HMR进行更新。这是维特最好的特点之一。当变化似乎立即反映出来,而不是等待,甚至是自己触发它们时,它会让开发体验变得更好。

这篇文章的其余部分将介绍我为使用Vite构建和运行自己的应用程序所必须做的所有事情。我希望其中一些与您相关!

Aliases(别名)

基于webpack的项目有这样的配置并不罕见:

resolve: {
  alias: {
    jscolor: "util/jscolor.js"
  },
  modules: [path.resolve("./"), path.resolve("./node_modules")]
}

这在提供的路径中为jscolor设置了一个别名,并告诉webpack在解析导入时同时查看根文件夹(./)和node_modules。这使我们可以进行这样的进口:

import { thing } from "util/helpers/foo"

…在组件树的任何位置,假设在最顶部有一个util文件夹。

Vite不允许您提供整个文件夹来进行这样的解析,但它允许您指定别名,这些别名遵循与@rollup/plugin-alias相同的规则:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

import path from "path";

export default defineConfig({
  resolve: {
    alias: {
      jscolor: path.resolve("./util/jscolor.js"),
      app: path.resolve("./app"),
      css: path.resolve("./css"),
      util: path.resolve("./util")
    }
  },
  plugins: [react()]
});

我们添加了resolve.alias部分,其中包括我们需要别名的所有条目。我们的jscolor-util被设置为相关模块,并且我们的顶级目录有别名。现在,我们可以从任何位置的任何组件导入app/、css/和util/。

请注意,这些别名仅适用于导入的根,例如util/foo。如果您的树中有其他util文件夹,并且您使用以下内容引用它:

import { thing } from "./helpers/util";

……那么上面的别名就不会搞砸了。这种区别没有很好的记录,但您可以在Rollup别名插件中看到它。Vite的别名与相同的行为相匹配。

环境变量

Vite当然支持环境变量。它从开发中的.env文件或process.env中读取配置值,并将它们注入到代码中。不幸的是,事情的工作方式与你可能习惯的有点不同。首先,它不能取代process.env。FOO,但更重要的是.meta.env。FOO。不仅如此,默认情况下,它只替换前缀为VITE_的变量。所以,import.meta.env。VITE_FOO实际上会被替换,但不是我原来的FOO。可以配置此前缀,但不能将其设置为空字符串。

对于遗留项目,您可以将所有环境变量进行grep和替换以使用import.meta.env,然后添加VITE_前缀,更新.env文件,并更新您使用的任何CI/CD系统中的环境变量。或者,您可以配置更经典的替换process.env的行为。任何在开发中具有.env文件值的东西,或在生产中具有真实进程.env值的东西。

方法如下。Vite的define功能基本上就是我们所需要的。这在开发过程中注册全局变量,并为生产进行原始文本替换。我们需要进行设置,以便在开发模式下手动读取.env文件,在生产模式下手动阅读process.env对象,然后添加适当的define条目。

让我们将所有这些构建到Vite插件中。首先,运行npm i dotenv。

现在让我们来看一下插件的代码:

import dotenv from "dotenv";

const isProd = process.env.NODE_ENV === "production";
const envVarSource = isProd ? process.env : dotenv.config().parsed;

export const dotEnvReplacement = () => {
  const replacements = Object.entries(envVarSource).reduce((obj, [key, val]) => {
    obj[`process.env.${key}`] = `"${val}"`;
    return obj;
  }, {});

  return {
    name: "dotenv-replacement",
    config(obj) {
      obj.define = obj.define || {};
      Object.assign(obj.define, replacements);
    }
  };
};

Vite设置process.env。NODE_ENV,所以我们所需要做的就是检查一下,看看我们处于哪种模式。

现在我们得到实际的环境变量。如果我们在生产中,我们会获取process.env本身。如果我们在dev中,我们要求dotenv获取我们的.env文件,解析它,并返回一个包含所有值的对象。

我们的插件是一个返回Vite插件对象的函数。我们将环境值注入到一个具有process.env的新对象中。在值前面,然后我们返回实际的插件对象。有许多挂钩可供使用。不过,在这里,我们只需要配置挂钩,它允许我们修改当前的配置对象。如果不存在定义条目,则添加一个定义条目,然后添加所有值。

但在继续前进之前,我想指出,我们正在解决的Vite环境变量限制是有原因的。上面的代码是捆绑器的频繁配置方式,但这仍然意味着process.env中的任何随机值都会被卡在源代码中(如果该键存在的话)。那里存在潜在的安全问题,所以请记住这一点。

代理配置

您部署的web应用程序是什么样子的?如果它所做的只是提供JavaScript/CSS/HTML——实际上一切都是通过位于其他地方的独立服务进行的——那就太好了!你实际上已经完成了。我给你看的应该就是你所需要的。Vite的开发服务器将根据需要为您的资产提供服务,它会像以前一样ping您的所有服务。

但是,如果你的网络应用程序足够小,以至于你的网络服务器上正在运行一些服务,该怎么办?对于我正在转换的项目,我在web服务器上运行了一个GraphQL端点。为了进行开发,我启动了Express服务器,它以前知道如何为webpack生成的资产提供服务。我还启动了一个webpack监视任务来生成这些资产。

但随着Vite推出自己的开发服务器,我们需要启动Express服务器(在Vite使用的端口之外的另一个端口上),然后在那里对/graphql进行代理调用:

server: {
  proxy: {
    "/graphql": "http://localhost:3001"
  }
} 

这个消息告诉Vite,对/graphql的任何请求都应该发送到http://localhost:3001/graphql.

请注意,我们没有将代理设置为http://localhost:3001/graphql在配置中。相反,我们将其设置为http://localhost:3001并依靠Vite将/graphql部分(以及任何查询参数)添加到路径中。

构建库

作为一个快速的奖励部分,让我们简要讨论一下构建图书馆的问题。例如,如果您只想构建一个JavaScript文件,例如像Redux这样的库,该怎么办。没有相关的HTML文件,所以您首先需要告诉Vite要做什么:

build: {
  outDir: "./public",
  lib: {
    entry: "./src/index.ts",
    formats: ["cjs"],
    fileName: "my-bundle.js"
  }
}

告诉Vite将生成的捆绑包放在哪里,称之为什么,以及构建什么格式。请注意,我在这里使用CommonJS而不是ES模块,因为ES模块不会缩小(截至本文撰写之时),因为担心它可能会破坏树的抖动。

您可以使用vite构建来运行此构建。要启动监视并在更改时重建库,您需要运行:

$ vite build --watch

总结

Vite是一个令人难以置信的令人兴奋的工具。它不仅减轻了捆绑web应用程序的痛苦和眼泪,而且在这个过程中大大提高了捆绑的性能。它附带了一个速度极快的开发服务器,该服务器附带了热模块重新加载,并支持所有主要的JavaScript框架。如果你做网络开发——无论是为了好玩,这都是你的工作,或者两者兼而有之–我再怎么强烈地推荐它也不为过。

一些你可能不知道的跨浏览器DevTools功能

胖蔡阅读(117)

本文翻译自:Some Cross-Browser DevTools Features You Might Not Know

我在DevTools上花了很多时间,我相信你也会。有时我甚至会在它们之间来回切换,尤其是在调试跨浏览器问题时。DevTools很像浏览器本身——并非一个浏览器的DevTools中的所有功能都与另一个浏览器中的DevTools相同或受支持。

但有相当多的DevTools功能是可互操作的,甚至是我即将与您分享的一些鲜为人知的功能。

为了简洁起见,我在文章中使用“Chromium”来指代所有基于Chromium的浏览器,如Chrome、Edge和Opera。其中的许多DevTools提供了彼此完全相同的特性和功能,所以这只是我同时提及所有这些特性和功能的简写。

在DOM树中搜索节点

有时DOM树中充满了嵌套在其他节点中的节点,等等。这使得很难找到您要查找的确切节点,但您可以使用Cmd+F(macOS)或Ctrl+F(Windows)快速搜索DOM树。

此外,您还可以使用有效的CSS选择器(如.red)或XPath(如//div/h1)进行搜索。

在Chromium浏览器中,当您键入时,焦点会自动跳转到与搜索条件匹配的节点,如果您使用较长的搜索查询或大型DOM树,这可能会很烦人。幸运的是,您可以通过转到Settings (F1) → Preferences → Global → Search as you type → Disable.

在DOM树中定位节点后,可以滚动页面,通过右键单击节点并选择“滚动到视图”将节点带入视口。

从控制台访问节点

DevTools提供了许多不同的方式来直接从控制台访问DOM节点。

例如,您可以使用$0来访问DOM树中当前选定的节点。Chromium浏览器更进一步,允许您使用$1、$2、$3等访问按历史选择的逆时间顺序选择的节点。

Chromium浏览器允许您做的另一件事是将节点路径复制为document.querySelector形式的JavaScript表达式,方法是右键单击节点,然后选择copy→ 复制JS路径,然后可以使用该路径访问控制台中的节点。

这里有另一种直接从控制台访问DOM节点的方法:作为临时变量。此选项可通过右键单击节点并选择一个选项来使用。该选项在每个浏览器的DevTools中都有不同的标签:

  • Chromium:右键单击→ “存储为全局变量”
  • Firefox:右键单击→ “在控制台中使用”
  • Safari:右键单击→ “日志元素”

使用徽章将元素可视化

DevTools可以通过在节点旁边显示徽章来帮助可视化与某些属性匹配的元素。徽章是可点击的,不同的浏览器提供各种不同的徽章。

在Safari中,“元素”面板工具栏中有一个徽章按钮,可用于切换特定徽章的可见性。例如,如果一个节点应用了display:grid或display:inline grid CSS声明,则会在其旁边显示一个网格徽章。单击徽章将在页面上高亮显示网格区域、轨迹大小、行号等。

Firefox的DevTools中当前支持的徽章列在Firefox源文档中。例如,滚动徽章表示可滚动元素。单击徽章会高亮显示导致溢出的元素,旁边会有一个溢出徽章。

在Chromium浏览器中,您可以右键单击任何节点并选择“徽章设置…”以打开一个列出所有可用徽章的容器。例如,具有滚动捕捉类型的元素旁边会有一个滚动捕捉徽章,单击后会切换该元素上的滚动捕捉覆盖。

截屏

我们已经能够从一些DevTools中进行屏幕截图有一段时间了,但现在它在所有DevTools中都可用,并包括新的全页截图方式。

该过程首先右键单击要捕获的DOM节点。然后选择捕获节点的选项,根据您使用的DevTools,节点会有不同的标记。

在html节点上重复相同的步骤以获取完整页面的屏幕截图。不过,当你这样做时,值得注意的是Safari保留了元素背景色的透明度——Chromium和Firefox会将其捕捉为白色背景。

还有另一种选择!您可以拍摄页面的“响应式”屏幕截图,这允许您以特定的视口宽度捕获页面。正如你所料,每个浏览器都有不同的方法来实现这一目标。

  • Chromium:Cmd+Shift+M(macOS)或Ctrl+Shift+M(Windows)。或者单击“检查”图标旁边的“设备”图标。
  • Firefox:工具→ 浏览器工具→ “响应式设计模式”
  • Safari:开发→ “进入响应式设计模式”

Chrome提示:检查顶层

Chrome允许您可视化和检查顶层元素,如对话框、警报或模态。当一个元素被添加到#top layer时,它会在旁边获得一个top layer徽章,单击后会跳转到位于标记后面的top layer容器。

顶层容器中元素的顺序遵循堆叠顺序,这意味着最后一个元素在顶部。单击显示徽章以跳回节点。

Firefox提示:跳转到ID

Firefox将引用ID属性的元素链接到同一DOM中的目标元素,并用下划线突出显示它。使用CMD+Click(macOS)或CTRL+Click(Windows))跳转到具有标识符的目标元素。

总结

很多事情,对吧?Chromium、Firefox和Safari都支持一些非常有用的DevTools功能,这真是太棒了。你喜欢这三种功能,还有其他鲜为人知的功能吗?

我有一些资源可以随时掌握最新动态。我想在这里分享一下:

  • DevTools提示(Patrick Brosset):精心策划的跨浏览器DevTools提示和技巧集。
  • Dev提示(Umar Hansa):DevTools提示发送到您的收件箱!
  • 我可以开发工具吗?:DevTools的容器。

Preact 表单使用

胖蔡阅读(115)

Preact 中的表单和 HTML 的一样:渲染一个控件,再为其添加事件监听器。

两者的差别在于大多数情况下 Preact 会为您自动控制 value,而不是由 DOM 节点控制。

约束性和非约束性组件

谈到表单控件时,您可能会时常听到约束性和非约束性组件 ((Un)Controlled Component) 的概念。约束性一词指数据流处理的方式。DOM 是双向的数据流,每个表单控件都会为用户管理输入。比如,一个文本框会自动将其值更新为用户输入的值。

Preact 一类的框架通常使用单向数据流,用组件树中更高的元素来管理数据。

// 非约束性组件,因为 Preact 没有为其设置数值
<input onInput={myEventHandler} />;

// 约束性组件,因为 Preact 管理其输入数值
<input value={someValue} onInput={myEventHandler} />;

您应当尽量使用约束性组件。但如果您是在构建独立组件,或包装第三方 UI 库,那么您可以用您的组件提供非 Preact 功能。这种情况下,您更需要非约束性组件。

此处要注意的是,将值设为 undefined 或 null 等同于非约束性组件。

构建简单表单

我们来创建一个提交待办事项的简单表单吧。为此,我们需要创建一个元素,并为其绑定表单提交时会触发的事件监听器,文本框亦然。但请注意,我们不会在类内存储值,而是使用约束性输入组件。此例中,这种方式非常适合我们在其他元素中显示文本框的值。

class TodoForm extends Component {
  state = { value: '' };

  onSubmit = e => {
    alert("Submitted a todo");
    e.preventDefault();
  }

  onInput = e => {
    this.setState({ e.currentTarget.value })
  }

  render(_, { value }) {
    return (
      <form onSubmit={this.onSubmit}>
        <input type="text" value={value} onInput={this.onInput} />
        <p>您输入了:{value}</p>
        <button type="submit">提交</button>
      </form>
    );
  }
}


选择菜单

<select> 元素有点复杂,但大致工作原理和其他控件差不多:

class MySelect extends Component {
  state = { value: '' };

  onChange = e => {
    this.setState({ value: e.currentTarget.value });
  }

  onSubmit = e => {
    alert("Submitted " + this.state.value);
    e.preventDefault();
  }

  render(_, { value }) {
    return (
      <form onSubmit={this.onSubmit}>
        <select value={value} onChange={this.onChange}>
          <option value="A">A</option>
          <option value="B">B</option>
          <option value="C">C</option>
        </select>
        <button type="submit">提交</button>
      </form>
    );
  }
}

复选框和单选按钮

复选框和单选按钮 () 会在非约束性的表单环境中引发混乱。通常情况下,我们需要浏览器帮我们勾选复选框或按钮,或是监听事件并对新传入的值做出反馈。但是,这种情况不适用于 UI 会自动根据状态和属性更新而自动更新的情况。

详细解释:先假设我们需要监听复选框在用户勾选/反选时触发的 “change” 事件。在事件监听器中,我们将 state 的值设置为复选框传入的值。这样,组件会被重新渲染,导致复选框再次被赋上状态中的值。但我们刚刚才从 DOM 获取值,再次渲染很明显是多此一举。

所以,我们需要监听用户点击复选框或相关 时触发的 click 事件,而非 input 事件。这样,复选框只会在 true 和 false 间切换。再次点击复选框或标签时,我们会将状态的布尔值反转,重新渲染,最后将复选框显示的值设置为我们期望的值。

复选框示例

class MyForm extends Component {
  toggle = e => {
      let checked = !this.state.checked;
      this.setState({ checked });
  };

  render(_, { checked }) {
    return (
      <label>
        <input
          type="checkbox"
          checked={checked}
          onClick={this.toggle}
        />
        选中这个复选框
      </label>
    );
  }
}

Bpmn-js自定义Palette

胖蔡阅读(170)

bpmn-js 阅读指南:

Bpmn-js作为一个流程编辑器,常规的我们可以将其划分为几个功能区域,每个区域对应的负责不同的功能实现,bpmn-js的设计给我们留下了大量的留白和可扩展区域,其每一部分都可进行组合拼装,同时也支持我们的各种不同层次需求的自定义操作。其常规区域划分如下:

本文主要介绍如何进行左侧工具栏palette的自定义,如何创建一个新的palette

回顾一下

同行我们都是通过bpmn-js中提供的modeler来创建编辑器主体,其核心代码如下:

<template>
    <div class="my-process-designer__container">
      <div class="my-process-designer__canvas" ref="bpmn-canvas"></div>
    </div>
<template>

<script>
import BpmnModeler from "bpmn-js/lib/Modeler";

const bpmnModeler = new BpmnModeler({
        container: this.$refs["bpmn-canvas"],
        keyboard: this.keyboard ? { bindTo: document } : null,
        additionalModules: this.additionalModules,
        moddleExtensions: this.moddleExtensions
      });
</script>

如上所示,有几个关键的属性:

  • container:当前绑定绘制的元素
  • keyboard:绑定键盘响应的元素
  • additionalModules:自定义扩展插件部分【我们需要自定义的部分就放这里】
  • moddleExtensions:用来进行 xml 字符串解析以及元素、属性实例定义的声明,常为外部引入的一个json文件或者js对象,这里是用来配置解析flowableactiviticamunda等不同BPMN 2.0框架声明的配置

自定义Palette

接下来我们就开始自定义一个Palette小工具,可以帮助我们业务快速识别需要的组件是哪个,当前组件使用已有bpmn:ServiceTask类型shape图片绘制。

创建工具提供者

这里我们需要创建一个小工具的提供者 TestPaletteProvider.js文件

import icon from '../../../../assets/bpmn/email.svg'

export default class TestPaletteProvider{
  // 自定义邮件收发组件
  constructor(palette, create, elementFactory) {

    this.create = create
    this.elementFactory = elementFactory
    palette.registerProvider(this)
  }

  // 这个函数就是绘制palette的核心
  getPaletteEntries(element) {
    const elementFactory = this.elementFactory
    const create = this.create

    function startCreate(event) {
      const serviceTaskShape = elementFactory.create(
        'shape', { type: 'bpmn:ServiceTask' },
      )

      create.start(event, serviceTaskShape)
    }

    return {
      'create-test-task': {
        group: 'activity',
        title: '创建测试元素',

        imageUrl: icon,
        action: {
          dragstart: startCreate,
          click: startCreate,
        },
      },
    }
  }
}

TestPaletteProvider.$inject = [

  'create',
  'elementFactory',
  'palette',

]

上面需要一个工具图示,这里是上面的svg,也可以自己找一个喜欢的图片:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1707209714780"
    class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1928" width="28"
    height="48" xmlns:xlink="http://www.w3.org/1999/xlink">
    <path
        d="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 972.8a460.8 460.8 0 1 1 460.8-460.8 460.8 460.8 0 0 1-460.8 460.8z"
        fill="#3B86E0" p-id="1929"></path>
    <path
        d="M734.72 332.8H289.28l222.72 148.48 222.72-148.48zM256 371.968v282.88l169.728-169.728L256 371.968zM526.08 533.248a25.6 25.6 0 0 1-28.16 0l-28.672-18.944L292.352 691.2h439.296l-176.896-176.896zM768 654.848V371.968l-169.728 113.152L768 654.848z"
        fill="#3B86E0" p-id="1930"></path>
</svg>

暴露插件

当前目录将该插件暴露出去,如下:

// index.js
import TestPaletteProviderfrom './TestPaletteProvider.js'

export default {
  __init__: ['testPalette'],
  testPalette: ['type', TestPaletteProvider],
}

应用

将自定义的palette应用在BpmnModeler上:

<script>
import BpmnModeler from "bpmn-js/lib/Modeler";
import testPalette from './extension-palette/test'

const bpmnModeler = new BpmnModeler({
        container: this.$refs["bpmn-canvas"],
        keyboard: this.keyboard ? { bindTo: document } : null,
        additionalModules: [testPalette ], // 在这里添加testPalette 插件
        moddleExtensions: this.moddleExtensions
      });

</script>

如上左侧就是我们的自定义的小工具,绘制区域我们使用的是bpmn:ServiceTask类型shape,绘制其实也可以自定义,这里后续介绍。希望大家能喜欢~

Preact中文文档 – 组件

胖蔡阅读(116)

组件为渲染结果添加状态,更是 Preact 的基石和构建复杂界面的基础。我们将在此教程中展示 Preact 中的两种组件。


函数组件

函数组件是接受 props 作为参数的函数,其名称必须以大写字母开头才能在 JSX 中使用。

function MyComponent(props) {
  return <div>我的名字叫{props.name}。</div>;
}

// 用法
const App = <MyComponent name="张三" />;

// 渲染结果:<div>我的名字叫张三。</div>
render(App, document.body);

请注意,在先前的版本中我们将其称之为“无状态组件”,但有了钩子组件后就不是了。


类组件

类组件可以拥有状态及生命周期方法,后者指组件被加到 DOM /从 DOM 中删除时会调用的特殊方法。

下面是一个显示当前时间的简单类组件 <Clock>

class Clock extends Component {

  constructor() {
    super();
    this.state = { time: Date.now() };
  }

  // 生命周期:在组件创建时调用
  componentDidMount() {
    // 每秒钟更新一次时间
    this.timer = setInterval(() => {
      this.setState({ time: Date.now() });
    }, 1000);
  }

  // 生命周期:在组件被摧毁时调用
  componentWillUnmount() {
    // 在无法渲染时停止时钟
    clearInterval(this.timer);
  }

  render() {
    let time = new Date(this.state.time).toLocaleTimeString();
    return <span>{time}</span>;
  }
}

生命周期方法

为了让时钟能每秒钟更新一次事件,我们需要知道 <Clock> 什么时候会被挂载到 DOM 上。如果您用过 HTML5 自定义元素的话,您就会发现这和 attachedCallback detachedCallback 生命周期方法很像。Preact 会自动为组件调用下列列表中存在的生命周期方法 :

生命周期方法被调用时间
componentWillMount()(已弃用) 组件将被挂载到 DOM 前调用
componentDidMount()组件被挂载到 DOM 后调用
componentWillUnmount()组件将从 DOM 移除前调用
componentWillReceiveProps(nextProps, nextState)(已弃用) 在传递进新属性前调用
getDerivedStateFromProps(nextProps)在 shouldComponentUpdate 前调用,请小心使用!
shouldComponentUpdate(nextProps, nextState)在 render() 前调用,返回 false 来跳过渲染
componentWillUpdate(nextProps, nextState)(已弃用) 在 render() 前调用
getSnapshotBeforeUpdate(prevProps, prevState)在 render() 前调用,返回值将传递进 componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)在 render() 后调用

异常捕获

有一个生命周期方法需要您特别注意,那就是 componentDidCatch。其特别之处是您可以使用此方法处理渲染中的错误,包括生命周期钩子中的错误,但不包括如 fetch() 在内的异步调用所产生的错误。

当捕获到错误时,我们可以使用此生命周期方法处理错误、显示错误信息或其他备用内容。


class Catcher extends Component {

  constructor() {
    super();
    this.state = { errored: false };
  }

  componentDidCatch(error) {
    this.setState({ errored: true });
  }

  render(props, state) {
    if (state.errored) {
      return <p>出现严重错误</p>;
    }
    return props.children;
  }
}


片段 (Fragment)

Fragment允许您同时返回多个元素。它们解决了JSX的限制,即每个“块”都必须有一个根元素。您经常会遇到它们与列表、表或CSS flexbox的组合,其中任何中间元素都会影响样式。

import { Fragment, render } from 'preact';

function TodoItems() {
  return (
    <Fragment>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </Fragment>
  )
}

const App = (
  <ul>
    <TodoItems />
    <li>D</li>
  </ul>
);

render(App, container);
// Renders:
// <ul>
//   <li>A</li>
//   <li>B</li>
//   <li>C</li>
//   <li>D</li>
// </ul>

请注意,大多数现代transfiler允许您对Fragments使用较短的语法。较短的一种更常见,也是你通常会遇到的一种。

// This:
const Foo = <Fragment>foo</Fragment>;
// ...is the same as this:
const Bar = <>foo</>;

您还可以从组件返回数组:

function Columns() {
  return [
    <td>Hello</td>,
    <td>World</td>
  ];
}

如果在循环中创建片段,请不要忘记将关键点添加到片段中:

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // Without a key, Preact has to guess which elements have
        // changed when re-rendering.
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}