Fork me on GitHub
艺术码畜的生活瞬间

pinia

pinia

一、Vuex 与 Pinia 核心思想与用法

Flux 架构

Flux 是 Facebook 在构建大型 Web 应用程序时为了解决数据一致性问题而设计出的一种架构,它是一种描述状态管理的设计模式。绝大多数前端领域的状态管理工具都遵循这种架构,或者以它为参考原型。

Flux 架构主要有四个组成部分:

  • 📦 store:状态数据的存储管理中心,可以有多个,可以接受 action 做出响应。
  • 🖼 view:视图,根据 store 中的数据渲染生成页面,与 store 之间存在发布订阅关系。
  • 🛠 action:一种描述动作行为的数据对象,通常会包含动作类型 type 和需要传递的参数 payload 等属性。
  • 📮 dispatcher:调度器,接收 action 并分发至 store。

整个数据流动关系为:

1、view 视图中的交互行为会创建 action,交由 dispatcher 调度器。

2、dispatcher 接收到 action 后会分发至对应的 store。

3、store 接收到 action 后做出响应动作,并触发 change 事件,通知与其关联的 view 重新渲染内容。

这就是 Flux 架构最核心的特点:单向数据流。

与传统的 MVC 架构相比,单向数据流也带来了一个好处:可预测性。

所有对于状态的修改都需要经过 dispatcher 派发的 action 来触发的,每一个 action 都是一个单独的数据对象实体,可序列化,操作记录可追踪,更易于调试。

Vuex 与 Pinia 大体上沿用 Flux 的思想,并针对 Vue 框架单独进行了一些设计上的优化。

Vuex

  • 📦 state:整个应用的状态管理单例,等效于 Vue 组件中的 data,对应了 Flux 架构中的 store。
  • 🧮 getter:可以由 state 中的数据派生而成,等效于 Vue 组件中的计算属性。它会自动收集依赖,以实现计算属性的缓存。
  • 🛠 mutation:类似于事件,包含一个类型名和对应的回调函数,在回调函数中可以对 state 中的数据进行同步修改。
    • Vuex 不允许直接调用该函数,而是需要通过 store.commit 方法提交一个操作,并将参数传入回调函数。
    • commit 的参数也可以是一个数据对象,正如 Flux 架构中的 action 对象一样,它包含了类型名 type 和负载 payload
    • 这里要求 mutation 中回调函数的操作一定是同步的,这是因为同步的、可序列化的操作步骤能保证生成唯一的日志记录,才能使得 devtools 能够实现对状态的追踪,实现 time-travel。
  • 🔨 action:action 内部的操作不受限制,可以进行任意的异步操作。我们需要通过 dispatch 方法来触发 action 操作,同样的,参数包含了类型名 type 和负载 payload
    • action 的操作本质上已经脱离了 Vuex 本身,假如将它剥离出来,仅仅在用户(开发者)代码中调用 commit 来提交 mutation 也能达到一样的效果。
  • 📁 module:store 的分割,每个 module 都具有独立的 state、getter、mutation 和 action。
    • 可以使用 module.registerModule 动态注册模块。
    • 支持模块相互嵌套,可以通过设置命名空间来进行数据和操作隔离。

Pinia

保留:

  • 📦 state:store 的核心,与 Vue 中的 data 一致,可以直接对其中的数据进行读写。
  • 🧮 getters:与 Vue 中的计算属性相同,支持缓存。
  • 🔨 actions:操作不受限制,可以创建异步任务,可以直接被调用,不再需要 commit、dispatch 等方法。

舍弃:

  • 🛠 mutation:Pinia 并非完全抛弃了 mutation,而是将对 state 中单个数据进行修改的操作封装为一个 mutation,但不对外开放接口。可以在 devtools 中观察到 mutation。
  • 📁 module:Pinia 通过在创建 store 时指定 name 来区分不同的 store,不再需要 module。

二、使用Pinia

1.安装pinia

npm i pinia -S //安装pinia依赖

2.main.js组册

//main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'

createApp(App)
.use(createPinia())
.mount('#app')

3.创建store

//countStore.js
import { defineStore } from 'pinia'
const useCountStore = defineStore('countStore',{
    state(){
        return {
            count:0
        }
    }
} )
export default useCountStore; //导出

4.新建组件使用state

//components/Count.vue
<script setup>
import useCountStore from '../store/countStore'
const countStore = useCountStore();
console.log(countStore)
</script>
<template>
  <div>
    {{countStore.count}}
  </div>
</template>
<style lang="css">
div{
    width: 100px;
    height: 30px;
    border:1px solid black;
    border-radius: 5px;
}
</style>
//App.vue
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import Count from './components/Count.vue'
</script>

<template>
  <div>
    <Count/>
  </div>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

countStore

我们可以直接使用就可以了,根据上面控制台输出可以看清楚countStore

到现在已经实现了创建store及使用

5.更新store

1.我们添加一个action方法

//countStore.js
import { defineStore } from 'pinia'

const useCountStore = defineStore('countStore',{
    //定义state
    state(){
        return {
            count:0
        }
    },
    //定义action方法
    actions:{
        add(){
            this.count++
        }
    }
} )

export default useCountStore;

2.解构方法和state

//Count.vue
<script setup>
import { storeToRefs } from 'pinia'
import useCountStore from '../store/countStore'
const {count,add} = storeToRefs(useCountStore());
</script>
<template>
  <div>
    {{count}}
  </div>
  <button @click="add()">Add</button>
</template>
<style lang="css">
div{
    width: 100px;
    height: 30px;
    border:1px solid black;
    border-radius: 5px;
    text-align: center;
}
button{
    position: relative;
    bottom: -30px;
}
</style> 

如果我们不使用 storeToRefs 来转发,那么页面将不会有任何响应

但是我们包裹了useCountStore()并且解构了,发现控制台输出错误error

经过修改发现,actions方法不可以通过解构来使用,只能通过另一种方法

参考文档:

[Pinia中文文档]: “https://pinia.web3doc.top/
[Vite中文文档]: “https://vitejs.cn/

vite配置

vite配置

最直观的体验是“快速”、“清爽”,开发效率也得到明显提升(解决掉了webpack开发服务器存在的痛点)。

初始化完成后,工程目录如下:

VS Code插件:Vite

配置详情:

vite.config.ts

import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJSX from '@vitejs/plugin-vue-jsx';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import { resolve } from 'path';
import { initDevProxy } from './http-proxy';

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  return {
    // 自定义公共基础路径(取自 .env(.*) 文件)
    base: env.VITE_BASE_PATH,
    // 定义全局常量替换方式
    define: {
      // 取消启用Vue.2x中选项式api(这部分不会被打包)
      '__VUE_OPTIONS_API__': false,
    },
    // 配置vite插件
    plugins: [
      vue(),
      vueJSX(),
      // 自动化的组件按需引入配置
      Components({
        extensions: ['vue', 'tsx'],
        resolvers: [
          AntDesignVueResolver({ resolveIcons: true }),
        ],
        include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/],
      }),
    ],
    // 配置解析项
    resolve: {
      // 路径别名
      alias: {
        '@/': resolve(__dirname, 'src') + '/',
      },
    },
    // 样式处理配置项
    css: {
      // CSS module
      modules: {
        generateScopedName: `_[name]_[local]_[hash:base64:6]_`,
      },
      // 传递给特定css样式预处理器的配置
      preprocessorOptions: {
        less: {
          javascriptEnabled: true,
        },
      },
    },
    // 配置开发服务器
    server: {
      open: true,
      proxy: initDevProxy(),
    },
    // 自定义打包项配置
    build: {
      // 大文件告警阈值(kb)
      chunkSizeWarningLimit: 400,
    },
  };
});

http-proxy.ts

import type { ProxyOptions } from 'vite';

/**
 * 初始化开发服务器代理
 */
export const initDevProxy = (): Record<string, string | ProxyOptions> => {
  return {
    // 临时示例
    '/api': {
      target: '',
      changeOrigin: true,
    },
  };
};

以上的配置是项目配置的一部分,其他所需要的配置按照自己的需要去官网查询就可以,官网基于没个配置都有详细讲解。

参考链接:

[Vite中文官网]: “https://cn.vitejs.dev/guide/

Vite+Vue3+Ts搭建私有组件库2

Vite+Vue3+Ts搭建私有组件库2

本文章只供参考,具体实现可能会有出入(会尽快完善)

一、vite打包

上一篇末尾的打包还是有缺陷的,打包的组件库只能给js项目使用,在ts项目下运行会出现一些错误,而且使用的时候还会失去代码提示功能,这样的话我们就失去了用ts开发组件库的意义了。所以我们需要在打包的库里加入声明文件(.d.ts)。

那么如何向打包后的库里加入声明文件呢? 其实很简单,只需要引入 vite-plugin-dts

pnpm i vite-plugin-dts -D -w

如果pnpm命令一直error,用其他命令下载安装也是一样的(下载到根目录)

然后修改一下我们的vite.config.ts引入这个插件

//components/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'

export default defineConfig(
    {
        build: {...},
        plugins: [
            vue(),
            dts({
                //指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
                tsConfigFilePath: '../../tsconfig.json'
            }),
            //因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
            dts({
                outputDir:'lib',
                tsConfigFilePath: '../../tsconfig.json'
            })

        ]
    }
)

执行pnpm run build你就会发现你的es和lib下就有了声明文件

二、组件样式

首先我们需要做的是将less打包成css然后放到打包后对应的文件目录下,我们在components下新建build文件夹来存放我们的一些打包工具,然后新建buildLess.ts,首先我们需要先安装一些工具cpy和fast-glob

pnpm i cpy fast-glob -D -w

它可以直接复制我们规定的文件并将我们的文件copy到指定目录,比如buildLess.ts:

//build/buildLess.ts
import cpy from 'cpy'
import { resolve } from 'path'

const sourceDir = resolve(__dirname, '../src')
//lib文件
const targetLib = resolve(__dirname, '../lib')
//es文件
const targetEs = resolve(__dirname, '../es')
console.log(sourceDir);
const buildLess = async () => {
    await cpy(`${sourceDir}/**/*.less`, targetLib)
    await cpy(`${sourceDir}/**/*.less`, targetEs)
}
buildLess()

然后在package.json中新增命令

...
"scripts": {
    "build": "vite build",
    "build:less": "esno build/buildLess"
  },
...

终端执行 pnpm run build:less 你就会发现lib和es文件对应目录下就出现了less文件.(如果error,切换node版本)

但是我们最终要的并不是less文件而是css文件,所以我们要将less打包成css,所以我们需要用的less模块.在ts中引入less因为它本身没有声明文件所以会出现类型错误,所以我们要先安装它的 @types/less

pnpm i --save-dev @types/less -D -w

buildLess.ts如下(详细注释都在代码中)

import cpy from 'cpy'
import { resolve } from 'path'
import { promises as fs } from "fs"
import less from "less"
import glob from "fast-glob"
const sourceDir = resolve(__dirname, '../src')
//lib文件目录
const targetLib = resolve(__dirname, '../lib')
//es文件目录
const targetEs = resolve(__dirname, '../es')



const buildLess = async () => {
    //直接将less文件复制到打包后目录
    await cpy(`${sourceDir}/**/*.less`, targetLib)
    await cpy(`${sourceDir}/**/*.less`, targetEs)

    //获取打包后.less文件目录(lib和es一样)
    const lessFils = await glob("**/*.less", { cwd: targetLib, onlyFiles: true })

    //遍历含有less的目录
    for (let path in lessFils) {
        const lessPathLib = `${targetLib}/${lessFils[path]}`
        const lessPathEs = `${targetEs}/${lessFils[path]}`

        //获取less文件字符串
        const lessCode = await fs.readFile(lessPathLib, 'utf-8')
        //将less解析成css
        const code = await less.render(lessCode)

        //拿到.css后缀path
        const cssPathLib = lessPathLib.replace('.less', '.css')
        const cssPathEs = lessPathEs.replace('.less', '.css')

        //将css写入对应目录
        await fs.writeFile(cssPathLib, code.css)
        await fs.writeFile(cssPathEs, code.css)
    }



}
buildLess()

执行打包命令之后你会发现对应文件夹下多了.css文件

现在我已经将css文件放入对应的目录下了,但是我们的相关组件并没有引入这个css文件;所以我们需要的是每个打包后组件的index.js中出现如:

import "xxx/xxx.css"

之类的代码我们的css才会生效;所以我们需要对vite.config.ts进行相关配置

首先我们先将.less文件忽略external: [‘vue’, /.less/],这时候打包后的文件中如button/index.js就会出现

import "./style/index.less";

然后我们再将打包后代码的.less换成.css就大功告成了

...
plugins: [
            ...

            {
                name: 'style',
                generateBundle(config, bundle) {
                    //这里可以获取打包后的文件目录以及代码code
                    const keys = Object.keys(bundle)

                    for (const key of keys) {
                        const bundler: any = bundle[key as any]
                        //rollup内置方法,将所有输出文件code中的.less换成.css,因为我们当时没有打包less文件

                        this.emitFile({
                            type: 'asset',
                            fileName: key,//文件名名不变
                            source: bundler.code.replace(/\.less/g, '.css')
                        })
                    }
                }
            }
        ...
        ]
...

我们最终的vite.config.ts如下

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'

export default defineConfig(
    {
        build: {
            target: 'modules',
            //打包文件目录
            outDir: "es",
            //压缩
            minify: false,
            //css分离
            //cssCodeSplit: true,
            rollupOptions: {
                //忽略打包vue和.less文件
                external: ['vue', /\.less/],
                input: ['src/index.ts'],
                output: [
                    {
                        format: 'es',
                        //不用打包成.es.js,这里我们想把它打包成.js
                        entryFileNames: '[name].js',
                        //让打包目录和我们目录对应
                        preserveModules: true,
                        //配置打包根目录
                        dir: 'es',
                        preserveModulesRoot: 'src'
                    },
                    {
                        format: 'cjs',
                        //不用打包成.mjs
                        entryFileNames: '[name].js',
                        //让打包目录和我们目录对应
                        preserveModules: true,
                        //配置打包根目录
                        dir: 'lib',
                        preserveModulesRoot: 'src'
                    }
                ]
            },
            lib: {
                entry: './index.ts',
                formats: ['es', 'cjs']
            }
        },




        plugins: [
            vue(),
            dts({
                //指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
                tsConfigFilePath: '../../tsconfig.json'
            }),
            //因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
            dts({
                outputDir: 'lib',
                tsConfigFilePath: '../../tsconfig.json'
            }),

            {
                name: 'style',
                generateBundle(config, bundle) {
                    //这里可以获取打包后的文件目录以及代码code
                    const keys = Object.keys(bundle)

                    for (const key of keys) {
                        const bundler: any = bundle[key as any]
                        //rollup内置方法,将所有输出文件code中的.less换成.css,因为我们当时没有打包less文件

                        this.emitFile({
                            type: 'asset',
                            fileName: key,//文件名名不变
                            source: bundler.code.replace(/\.less/g, '.css')
                        })
                    }
                }
            }

        ]
    }
)

最后我们将打包less与打包组件合成一个命令(package.json):

...
"scripts": {
     "build":"vite build",
    "build:less": "esno build/buildLess"
  },
...

后续直接执行pnpm run build 即可完成所有打包啦

三、引用ui包

做了那么多终于到发布的阶段了;其实npm发包是很容易的,就拿我们的组件alanmf举例吧

发布之前记得到npm官网注册个账户,如果你要发布@xx/xx这种包的话需要在npm新建个组织组织组织名就是@后面的,比如我建的组织就是kitty-ui,注册完之后你就可以发布了

首先要将我们代码提交到git仓库,不然pnpm发布无法通过,后面每次发版记得在对应包下执行 pnpm version patch你就会发现这个包的版本号patch(版本号第三个数) +1 了

如果你发布的是公共包的话,在对应包下执行

pnpm publish --access public

输入你的账户和密码(记得输入密码的时候是不显示的,不要以为卡了)正常情况下应该是发布成功了

注意

发布的时候要将npm的源切换到npm的官方地址(https://registry.npmjs.org/); 如果你使用了其它镜像源的话

Vite+Vue3+Ts搭建私有组件库

Vite+Vue3+Ts搭建私有组件库

目前前端技术的持续发展,社区生态出现了一批比较适用的组件库,例如:ElementUI,Vant,AntDesign等,但这些组件库的组件不一定能满足我们一些项目的特定业务需求,那么我们的团队就应该建立起我们自己的组件库,方便于我们自己的业务开发,那么我们这篇文章就来了解一下我们开发组件库的大体技术架构体系。

搭建组件库的技术要点:

  • 如何使用pnpm搭建出一个menorepo环境
  • 如何使用vite搭建一个基本的Vue3脚手架项目
  • 如何开发调试一个自己的UI组件库
  • 如何使用vite打包并发布自己的UI组件库

一、menorepo环境的搭建

首先我们要了解什么是menorepo及它是如何搭建 (单仓库 多项目)

就是指在一个大的项目仓库中,管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理。大概结构如下:

-- packages
  -- pkg1
    --package.json
  -- pkg2
    --package.json
--package.json

目前很多我们熟知的项目都是采用这种模式,如Vant,ElementUI,Vue3等。打造一个menorepo环境的工具有很多,如:lerna、pnpm、yarn等,这里我们将使用pnpm来开发我们的UI组件库

1.使用pnpm

npm install pnpm -g //安装pnpm
pnpm init //初始化package.json

新建配置文件 .npmrc

shamefully-hoist = true

这里简单说下为什么要配置shamefully-hoist。

如果某些工具仅在根目录的node_modules时才有效,可以将其设置为true来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的…都放到同一级别(扁平化)。说白了就是不设置为true有些包就有可能会出问题。

安装对应依赖

pnpm i vue@next typescript less -D -w //开发环境中的依赖一般全部安装在整个项目根目录下,方便下面我们每个包都可以引用,所以在安装的时候需要加个 -w

如果我们是在根目录下运行命令,那么就不用加 -w 了,要不然会提示

--workspace-root may only be used inside a workspace

配置tsconfit.json

npx tsc --init //初始化
//tsconfit.json配置
{
  "compilerOptions": {
    "baseUrl": ".",
    "jsx": "preserve",
    "strict": true,
    "target": "ES2015",
    "module": "ESNext",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "moduleResolution": "Node",
    "lib": ["esnext", "dom"]
  }
}

2.monorepo的实现

各个项目之间能够互相引用我们要新建一个pnpm-workspace.yaml文件将我们的包关联起来

packages:
    - 'packages/**'
    - 'examples'

这样就能将我们项目下的packages目录和examples目录关联起来了,当然如果你想关联更多目录你只需要往里面添加即可。根据上面的目录结构很显然你在根目录下新建packages和examples文件夹,packages文件夹存放我们开发的包,examples用来调试我们的组件。

examples文件夹就是接下来我们要使用vite搭建一个基本的Vue3脚手架项目的地方。

3.手动搭建一个基于vite的vue3项目

进入examples文件夹,执行

pnpm init //初始化仓库
pnpm install vite @vitejs/plugin-vue -D -w //安装vite和@vitejs/plugin-vue

配置vite.config.ts

新建vite.config.ts文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins:[vue()]
})

新建html文件

@vitejs/plugin-vue 会默认加载examples下的index.html

新建index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="main.ts" type="module"></script>
</body>
</html>

注意:
vite 是基于esmodule的 所以type=”module”

新建app.vue模板

<template>
    <div>
        启动测试
    </div>
</template>

新建main.ts

import {createApp} from 'vue'
import App from './app.vue'

const app = createApp(App)

app.mount('#app')

此时会发现编译器会提示个错误:找不到模块“./app.vue”或其相应的类型声明

因为直接引入.vue文件 TS会找不到对应的类型声明;所以需要新建typings(命名没有明确规定,TS会自动寻找.d.ts文件)文件夹来专门放这些声明文件。

typings/vue-shim.d.ts

TypeScriptTS默认只认ES 模块。如果你要导入.vue文件就要declare module把他们声明出来。

declare module '*.vue' {
    import type { DefineComponent } from "vue";
    const component:DefineComponent<{},{},any>
}

配置脚本启动项目

最后在package.json文件中配置scripts脚本

...
"scripts": {
    "dev": "vite"
  },
...

然后终端输入我们熟悉的命令:pnpm run dev

如果出现了cannot find module ‘node:path’这样的错误提示,那可能是node版本不兼容,升级一下node版本就可以了

我的node 版本是 v14.17.0 的版本,出现了不兼容的问题,于是升级到了稳定版 v16.16.0 。

再运行vite就没问题了

二、本地调试

1.新建包文件

往packages文件夹冲填充内容

一般packages要有utils包来存放我们公共方法,工具函数等

既然它是一个包,所以我们新建utils目录后就需要初始化它,让它变成一个包;终端进入utils文件夹执行:pnpm init 然后会生成一个package.json文件;这里需要改一下包名,我这里将name改成@alanmf/utils表示这个utils包是属于alanmf这个组织下的。所以记住发布之前要登录npm新建一个组织;例如alanmf

因为我们使用ts写的,所以需要将入口文件index.js改为index.ts,并新建index.ts文件:(先导出一个简单的加法函数)

components是我们用来存放各种UI组件的包

新建components文件夹并执行 pnpm init 生成package.json

  • 组件库包 这里命名为alanmf
{
  "name": "@alanmf/utils",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

新建index.ts入口文件并引入utils包

//@kitty-ui/utils
import {testfun} from '@alanmf/utils'

const result = testfun (1,1)

console.log(result)

由于组件库是基于ts的,所以需要安装esno来执行ts文件便于测试组件之间的引入情况

控制台输入esno xxx.ts即可执行ts文件

npm i esno -g //安装esno
eson index.ts //执行index.ts文件

2.包之间本地调试

进入components文件夹执行

pnpm install @alanmf/utils

你会发现pnpm会自动创建个软链接直接指向我们的utils包;此时components下的packages:

{
  "name": "alanmf",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@alanmf/utils": "workspace:^1.0.0"
  }
}

你会发现它的依赖@alanmf/utils对应的版本为:workspace:^1.0.0;因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用。

到这里基本开发方法我们已经知道啦;接下来就要进入正题了,开发一个button组件

三、开发一个button组件

1.创建一个测试组件

在components文件夹下新建src,同时在src下新建button组件目录和icon组件目录(新建icon为了便于调试);此时components文件目录如下

-- components
  -- src
    -- button
    -- icon
    -- index.ts
-- package.json

让我们先测试一下我们的button组件能否在我们搭建的examples下的vue3项目本引用~

首先在button下新建一个简单的button.vue

<template>
    <button>测试按钮</button>
</template>

然后在button/index.ts将其导出

import Button from './button.vue'

export default Button

因为我们开发组件库的时候不可能只有button,所以我们需要一个components/index.ts将我们开发的组件一个个的集中导出

import Button from './button'

export {
    Button
}

好了,一个组件的大体目录差不多就是这样了,接下来请进入我们的examples来看看能否引入我们的button组件

2.测试项目引入

上面已经说过执行在workspace执行 pnpm i xxx的时候pnpm会自动创建个软链接直接指向我们的xxx包。

所以这里我们直接在examples执行:pnpm i alanmf

此时你就会发现packages.json的依赖多了个

"alanmf": "workspace:^1.0.0"

这时候我们就能直接在我们的测试项目下引入我们本地的components组件库了,启动我们的测试项目,来到我们的 examples/app.vue 直接引入Button

<template>
    <div>
        <Button />
    </div>
</template>
<script lang="ts" setup>
import { Button } from 'alanmf/src'
</script>

不出意外的话你的页面就会展示我们刚刚写的button组件了

接下来的工作就是专注于组件的开发了;让我们回到我们的button组件目录下(测试页面不用关,此时我们已经可以边开发边调试边看效果了)

3.types.ts文件规范组件属性

我们的button组件是需要接收很多属性的,如type、size等等,所以我们要新建个types.ts文件来规范这些属性

在button目录下新建types.ts


import { ExtractPropTypes } from 'vue'


export const ButtonType = ['default', 'primary', 'success', 'warning', 'danger']

export const ButtonSize = ['large', 'normal', 'small', 'mini'];


export const buttonProps = {
  type: {
    type: String,
    values: ButtonType
  },
  size: {
    type: String,
    values: ButtonSize
  }
}

export type ButtonProps = ExtractPropTypes<typeof buttonProps>

import type 表示只导入类型;ExtractPropTypes是vue3中内置的类型声明,它的作用是接收一个类型,然后把对应的vue3所接收的props类型提供出来,后面有需要可以直接使用

很多时候我们在vue中使用一个组件会用的app.use 将组件挂载到全局。要使用app.use函数的话我们需要让我们的每个组件都提供一个install方法,app.use()的时候就会调用这个方法;

我们将button/index.ts调整为

import button from './button.vue'
import type {App,Plugin} from "vue"
type SFCWithInstall<T> = T&Plugin
const withInstall = <T>(comp:T) => {
    (comp as SFCWithInstall<T>).install = (app:App)=>{
        //注册组件
        app.component((comp as any).name,comp)
    }
    return comp as SFCWithInstall<T>
}
const Button = withInstall(button)
export default Button

此时我们就可以使用app.use来挂载我们的组件啦

其实withInstall方法可以做个公共方法放到工具库里,因为后续每个组件都会用到,这里等后面开发组件的时候再调整

到这里组件开发的基本配置已经完成,最后我们对我们的组件库以及工具库进行打包,打包之前如果要发公共包的话记得将我们的各个包的协议改为MIT开源协议

...
"license": "MIT",
...

四、vite打包

1.配置文件

打包们这里选择vite,它有一个库模式专门为我们来打包这种库组件的。

前面已经安装过vite了,所以这里直接在components下直接新建vite.config.ts(配置参数文件中已经注释):

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
export default defineConfig(
    {
        build: {
            target: 'modules',
            //打包文件目录
            outDir: "es",
            //压缩
            minify: false,
            //css分离
            //cssCodeSplit: true,
            rollupOptions: {
                //忽略打包vue文件
                external: ['vue'],
                input: ['src/index.ts'],
                output: [
                    {
                        format: 'es',
                        //不用打包成.es.js,这里我们想把它打包成.js
                        entryFileNames: '[name].js',
                        //让打包目录和我们目录对应
                        preserveModules: true,
                        //配置打包根目录
                        dir: 'es',
                        preserveModulesRoot: 'src'
                    },
                    {
                        format: 'cjs',
                        entryFileNames: '[name].js',
                        //让打包目录和我们目录对应
                        preserveModules: true,
                        //配置打包根目录
                        dir: 'lib',
                        preserveModulesRoot: 'src'
                    }
                ]
            },
            lib: {
                entry: './index.ts',
                formats: ['es', 'cjs']
            }
        },
        plugins: [
            vue()
        ]
    }
)

这里我们选择打包cjs(CommonJS)和esm(ESModule)两种形式,cjs模式主要用于服务端引用(ssr),而esm就是我们现在经常使用的方式,它本身自带treeShaking而不需要额外配置按需引入(前提是你将模块分别导出)

2.vite打包

到这里就已经可以直接打包了;components下执行: pnpm run build你就会发现打包了es和lib两个目录

参考链接:

[私有仓库搭建源码]: “https://github.com/mengfeng/alanmf_ui.git

Vue3+Ts+Vite+Pinia组合的高效开发套件

Vue3+Ts+Vite+Pinia组合的高效开发套件

2022年最受欢迎的组合

一、Vite和Webpack区别

  • webpack是一个JavaScript应用程序的静态模块打包工具,它会对整个应用程序进行依赖关系图构建。
  • vite是构建工具的高阶封装,使用简单,快(开发的时候感觉没有编译过程),便于扩展。而他集成的esbuild(Go 编写) 预构建依赖,比node快 10-100 倍。

区别:

1.关注层级不同

vite关注的层级更高:vite是 high level api,关注的是如何快速方便的搭建项目,相比webpack,减少了很多配置量。

webpack关注的层级更低:webpack是low level api,因为webpack更关注的是各种功能的实现,重点放在构建上。

2.vite自己不包含编译能力。

它本身并不参与编译,它的编译能力只是集成了rollup和ESbuild的功能.

3.启动项目vite更快,可以说是超级快。

对比webpack在dev-serve的时候,会提交所有编译的文件,而vite在dev-serve的时候利用了浏览器的native ES module功能,在浏览器请求对应的url时才提供文件,实现了根据路由的懒加载,所以启动的时候是超快的。

4.vite的热更新更快。

对比webpack的热更新,热更新时,把改动过模块的相关依赖模块全部编译一次。而vite热更新时,仅让浏览器重新请求改动过的模块。

现在公司项目从webpack过渡到vite,还在不断摸索爬坑中,虽然目前vite的生态不如webpack丰富,且实用的开发者也不及webpack。
但是不可否认的是,相比于webpack, vite非常适合项目的开发,webpack适合工具的开发。

  • vite为构建项目而生

  • webpack为构建工具而生

二、Pinia和Vuex

Pinia是Vue生态里Vuex的代替者,一个全新Vue的状态管理库。在Vue3成为正式版以后,尤雨溪强势推荐的项目就是Pinia。

Pinia 是 Vue.js 的轻量级状态管理库,它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。
Vuex也是为Vue框架建立的一个流行的状态管理库,它也是Vue核心团队推荐的状态管理库。 Vuex高度关注应用程序的可扩展性、开发人员的工效和信心。它基于与redux相同的流量架构。Pinia 完整的符合了当时 Vuex5 提案所提到的功能点。

Pinia 和 Vuex

Vuex: StateGettesMutations(同步)、Actions(异步)

Pinia: StateGettesActions(同步异步都支持)

Pinia 核心特性

  • Pinia 没有 Mutations

  • Actions 支持同步和异步

  • 没有模块的嵌套结构

    • Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系
  • 更好的 TypeScript 支持

    • 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断
  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便

  • 无需手动添加 store,它的模块默认情况下创建就自动注册的

  • Vue2 和 Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者使用上的API都是相同的
  • 支持 Vue DevTools

    • 跟踪 actions, mutations 的时间线
    • 在使用了模块的组件中就可以观察到模块本身
    • 支持 time-travel 更容易调试
    • 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用
    • 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能
  • 模块热更新

    • 无需重新加载页面就可以修改模块
    • 热更新的时候会保持任何现有状态
  • 支持使用插件扩展 Pinia 功能

  • 支持服务端渲染

三、Vite生成Vue3脚手架

npm create vite@latest //构建vite+vue
npm i //下载依赖
npm run dev //启动项目

四、集成pinia

npm install pinia //安装pinia

pinia简单使用

1.在main.ts中注册pinia

//main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import {createPinia} from 'pinia' 

createApp(App).use(createPinia()).mount('#app')

2.创建src/type/user.d.ts

// 用户的类型声明文件
interface IUser {
    name: string;
    age: number;
}  

3.创建src/store/user.ts

//user.ts
import { defineStore } from "pinia";
 
export default defineStore("user", {
  state() {
    return {
      userList: [] as IUser[],
    };
  },
  actions: {
    getList() {
      // 模拟从后端获取数据
      let resList: IUser[] = [
        { name: "孟峰", age: 24 },
        { name: "孟一", age: 19 }
      ];
      this.userList = resList;
    },
  },
});

4.创建src/store/index.ts

//index.ts
import useUserStore from "./user";
 
export default function useStore() {
  return {
    user: useUserStore(),
  };
}

5.App.vue使用

<script setup lang="ts">
// 从状态层导出指定模块
import useStore from "./store";
const { user } = useStore();
// 获取用户列表
user.getList();
</script>
 
<template>
  <div>
    <ul>
      <li v-for="item in user.userList">
        姓名:{{ item.name }} ---- 年龄:{{ item.age }}
      </li>
    </ul>
  </div>
</template>

五、集成pretter和eslint

npm i prettier -D //安装prettier
npm i eslint -D //安装eslint
npx eslint --init //初始化eslint

根目录创建.prettierrc.js文件

//.prettierrc.js
module.exports = {
    printWidth: 140,// 超过最大值换行
    tabWidth: 4, // tab键宽度,默认为4
    useTabs: true,// 使用tab(制表符)缩进而非空格
    singleQuote: true,// 用单引号代替双引号
    semi: true,// 行末是否加分号
    trailingComma: 'none',// 最后一个对象元素加逗号
    bracketSpacing: true,// 对象,数组加空格
    jsxBracketSameLine: true, // jsx > 是否另起一行
  
  };
//.eslintrc.cjs
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es6: true,
    commonjs: true,
    amd: true
  },
  // ts eslint 配置
  parserOptions: {
    parser:'@typescript-eslint/parser',
  },
  plugins: ['@typescript-eslint'],
  extends: ['plugin:vue/vue3-recommended','plugin:prettier/recommended','prettier/@typescript-eslint','plugin:@typescript-eslint/recommended'],
  // js eslint 配置
  // parserOptions: {
  //   parser: 'babel-eslint',
  //   sourceType: 'module'
  // },
  // plugins: ['html', 'vue'],
  // extends: ['plugin:vue/recommended', 'eslint:recommended'],
 
  rules: {
    'max-len': 'off',
     // 统一豁免规则,原因:直接修改可能对现有功能产生影响
     'eqeqeq': 1,
    //  'vue/no-v-html': 1,
     // 其中代码本身有问题的规则错误有
     'no-undef': 0,
     'import/no-duplicates': 0,
 
     // 可能引起格式化问题但建议手动修改代码的有
     'no-plusplus': 0,
     'no-eval': 0,
     'no-prototype-builtins': 0,
     'no-multi-assign': 0,
     'no-unused-vars': 0,
     'no-useless-escape': 0,
     'camelcase': 0,
     'vue/no-unused-components': 0,
     'vue/return-in-computed-property': 0,
     'no-param-reassign': 0,
     'prefer-const': 0,
     'prefer-destructuring': 0,
     'no-underscore-dangle': 0,
     'no-restricted-syntax': 0,
     'no-nested-ternary': 0,
     'radix': 0,
     'vue/no-side-effects-in-computed-properties': 0,
     'vue/order-in-components': 0,
     'function-paren-newline': 0,
  },
};

参考链接:

[git仓库项目源码]: “https://github.com/mengfeng/vite-vue3-pinia-ts
[Vite中文官网]: “https://cn.vitejs.dev
[pinia中文官网]: “https://pinia.web3doc.top/getting-started.html#安装

webgl

webgl

第一章 webgl绘图流程


一、创建canvas画布

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
            overflow: hidden;
        }
        #canvas{
            background-color: pink;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
</body>
</html>

二、js获取canvas画布

const canvas = document.querySelector('#canvas')
        canvas.width = window.innerWidth
        canvas.height = window.innerHeight

三、使用canvas获取webgl绘图上下文

 const gl = canvas.getContext("webgl")
        gl.clearColor(1,0,0,1)
        gl.clear(gl.COLOR_BUFFER_BIT)

四、在script中建立顶点着色器和顶点着色器(glsl es语言)

<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
        void main(){
            gl_Position = vec4(0,0,0,1);
            gl_PointSize = 50.0;
        }
</script>
    <!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
        void main(){
            gl_FragColor = vec4(1,1,1,1);
        }
</script>

五、在js中获取顶点着色器和片元着色器

//顶点着色器文本
const vsSource = document.getElementById('vertexShader').innerText;
//片元舍色器文本
const fsSourse = document.getElementById('fragmentShader').innerText;

六、初始化节点

 //初始化着色器
 initShaders(gl,vsSource,fsSourse);

七、指定将来用来清空绘图区的颜色

//指定将要用来清理绘图区的颜色
gl.clearColor(0,0,0,1);

八、使用指定颜色清空绘图区

//清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT);

九、绘制顶点

//绘制顶点(点模式)
gl.drawArrays(gl.POINTS,0,1);

十、初始化节点具体代码实现

function initShaders(gl,vsSource,fsSourse){
//创建程序对象
const program = gl.createProgram();
//建立着色对象
const vertexShader = loadShader(gl,gl.VERTEX_SHADER,vsSource);
const fragmentShader = loadShader(gl,gl.FRAGMENT_SHADER,fsSourse);
//将顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader);
//将片元着色对象装进程序对象中
         gl.attachShader(program,fragmentShader);
//连接webgl上下文对象和程序对象
gl.linkProgram(program);
//启动程序对象
gl.useProgram(program);
//将程序对象挂到上下文对象
gl.program = program;
return true;
}

function loadShader(gl,type,source){
//根据着色器的类型,建立着色器对象
const shader = gl.createShader(type);
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source);
//编译着色器对象
gl.compileShader(shader);
//返回着色器对象
return shader;
}

第二章 用js控制点位

一、js中声明attribute变量

<script id="vertexShader" type="x-shader/x-vertex">
//声明变量类型vec4,导出变量a_Position
        attribute vec4 a_Position;
//修改顶点尺寸和修改顶点位置是一样的原理,只是类型为float
        void main(){
            gl_Position = a_Position;
            gl_PointSize = 50.0;
        }
    </script>

二、js获取attribute变量

//设置attribute变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');

三、在js中修改attribute变量

//修改attribute变量值
gl.vertexAttrib3f(a_Position,0,0,0)
//参数(变量名,x,y,z)
//也有许多同族函数
gl.vertexAttrib1f(a_Position,0)
gl.vertexAttrib2f(a_Position,0,0)
gl.vertexAttrib3f(a_Position,0,0,0)
gl.vertexAttrib4f(a_Position,0,0,0,0)

案例小节(鼠标控制点位)

1.获取鼠标在canvas中的位置

//获取鼠标点在canvas中的位置
canvas.addEventListener('click',(event)=>{
const {clientX,clientY} = event;
const {left,top} = canvas.getBoundingClientRect();
const [cssX,cssY]=[clientX - left,clientY - top];
        })

//解决canvas和webgl差异
//1.解决原点差异
const [halfWidth,halfHeight] = [width/2,height/2];
const [xBaseCenter,yBaseCenter] = [cssX - halfWidth,cssY - halfHeight];
//2.解决y轴轴向差异
const yBaseCenterTop = -yBaseCenter;
//3.解决基底差异
const [x,y] = [xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];

2.绘制图层

gl.vertexAttrib2f(a_Position,x,y);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS,0,1);

四、webgl同步绘图原理

​ webgl 的同步绘图的现象,其实是由于 webgl 底层内置颜色缓冲区导致的.它在电脑中会占用一块内存,在我们使用 webgl 绘图的时候,是在颜色缓冲区中画出来,但是图片暂时还未渲染出来.只有 webgl 自己知道.如果我们想要将图像的时候,那就照着缓冲区的图像去画,这个步骤是 webgl 内部自己完成的,我们只需要执行绘图命令就行了,颜色缓冲区存储的图像,只有当前线程有效,比如我们先在 js 主线程绘图的时候,主线程结束后,会在执行信息队列的异步线程,子啊执行异步线程时,颜色缓冲区会被 webgl 重置,导致颜色缓冲器绘制的图形被清除,导致以前绘制的图像也会消失.

第三章 用js控制顶点颜色

一、js中声明uniform变量

<script id="fragmentShader" type="x-shader/x-fragment">
 //将float精度设置为中等
 precision mediump float;
 uniform vec4 u_FragColor;
 void main(){
    gl_FragColor = u_FragColor;
 }
</script>

二、js获取uniform变量

const u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');

三、在js中修改uniform变量值

//一个一个传递参数
gl.uniform4f(u_FragColor, 0.0, 1.0, 0.0, 1.0);
//可以以数组的形式传递参数
//Float32Array为一种32位浮点型数组,在浏览器中的运行效率比Array要高的多
const color = new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4v(u_FragColor,color);

四、片元着色器的绘图原理

​ 我们所看到的图形都是由多个片元组成的,那么想要绘制相应图形,只需要渲染范围内的片元就可以了

具体代码实现:

<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main(){
  //distance为计算两个点的距离 distance(p1,p2)
  //gl_PointCoord片元在一个点中的位置是统一化的
  //discard丢弃,不会进行渲染
float dist = distance(gl_PointCoord,vec2(0.5,0.5));
if(dist < 0.5){
  //进入范围就渲染
    gl_FragColor = u_FragColor;
}else{
    discard;
}
}
</script>

五、片元着色器合成功能

开启着色器合成功能和声明合成方式才可以生效

//开启片元着色器合成功能
gl.enable(gl.BLEND);
//设置片元着色器合成方式
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

第四章 架构补间动画

完成透明度动画

一、建立合成对象

export default class Compose {
    constructor(){
//parent 父对象,合成对象可以嵌套
        this.parent = null;
//children 子对象集合,其集合可以是时间轨,也可以是合成对象
        this.children = [];
    }
//添加子对象方法
    add(obj){
        obj.parent = this;
        this.children.push(obj);
    }
//基于当前时间更新子对象状态的方法
    update(t) {
        this.children.forEach((ele)=>{
            ele.update(t);
        })
    }
}

二、建立时间轨

export default class Track{
    constructor(target){
        this.target = target;
        this.parent = null;
        this.start = 0;
        this.timeLen = 5;
        this.loop = false;
        this.keyMap = new Map();
    }
    update(t) {
        const {target,start,timeLen,loop,keyMap} = this;
        //本地时间
        let time = t - start;
        if(loop){
            time = time % timeLen;
        }
        //遍历关键帧集合,判断在两个关键帧之前还是之后
        for(const [key,fms] of keyMap){
            const last = fms.length - 1;
            if(time < fms[0][0]){
                target[key] = fms[0][1]; 
            }else if(time > fms[0][1]){
                target[key] = fms[last][0];
            }else{
                target[key] = getValBetweenFms(time,fms,last);
            }
        }
    }
}

//获取两个关键帧之间的补间状态的方法
function getValBetweenFms(time,fms,last){
    for(let i = 0;i < last;i++){
        //两个关键帧fm1和fm2
        const fm1 = fms[i];
        const fm2 = fms[i + 1];
        //如果在两个关键帧之间,基于两个关键帧的时间和状态求点斜式
        if(time >= fm1[0] && time <= fm2[0]){
            const delta = {
                x: fm2[0] - fm1[0],
                y: fm2[1] - fm1[1],
            };
            //获取斜率k和截距b
            const k = delta.y / delta.x;
            const b = fm1[1] - fm1[0] * k;
            //根据点斜式求当前时间对应的状态
            return k * time + b;
        }
    }
}

三、使用合成对象和轨道对象制作补间动画

//引入合成对象Compose和轨道对象Track
import Compose from './utils/Compose.js'
import Track from './utils/ComposeOne.js'
const compose = new Compose();
const obj = {x,y,size};
starts.push(obj);
//建立轨道对象
const track = new Track(obj);
track.start = new Date();
track.timeLen = 2000;
track.loop = true;
track.keyMap = new Map([
            [
                'size',
                [
                    [500,size],
                    [1000,0],
                    [1500,size]
                ]
            ]
           ])
compose.add(track);
//连续渲染
!(function ani(){
            compose.update(new Date());
            render();
            requestAnimationFrame(ani);
 })()
 //渲染方法
 function render(){
gl.clear(gl.COLOR_BUFFER_BIT);
starts.forEach(({x,y,size})=>{
   gl.vertexAttrib2f(a_Position,x,y);
   gl.vertexAttrib1f(a_PointSize,size);
   gl.uniform4f(u_FragColor, 1.0, 1.0,1.0, Math.random()*1);
   
  gl.drawArrays(gl.POINTS,0,1);
            })
        }

第五章 绘制多点图形

一、绘制多点步骤

和之前的webgl绘图流程是一样的只是赋值attribute不一样

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>webgl多点绘制</title>
    <style>
        body{
            margin: 0;
            overflow: hidden;
        }
        #canvas{
            background-color: antiquewhite;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <!-- 顶点着色器 -->
    <script id="vertexShader" type="x-shader/x-vertex">
        attribute vec4 a_Position;
        void main(){
            gl_Position = a_Position;
            gl_PointSize = 50.0;
        }
    </script>
    <!-- 片元着色器 -->
    <script id="fragmentShader" type="x-shader/x-fragment">
        void main(){
            gl_FragColor = vec4(1,1,1,1);
        }
    </script>
    <script type="module">
        import {initShaders} from './utils/index.js'
        //canvas画布,获取上下文对象
        const canvas = document.querySelector('#canvas');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        //webgl画笔
        const gl = canvas.getContext("webgl");
        //顶点着色器文本
        const vsSource = document.getElementById('vertexShader').innerText;
        //片元舍色器文本
        const fsSourse = document.getElementById('fragmentShader').innerText;
        //初始化着色器
        initShaders(gl,vsSource,fsSourse);
      //设置attribute变量
        const a_Position =                        gl.getAttribLocation(gl.program, 'a_Position');
        //在这开始修改

        //指定将要用来清理绘图区的颜色
        gl.clearColor(0,0,0,1);
        //清理绘图区
        gl.clear(gl.COLOR_BUFFER_BIT);
        //绘制顶点
        gl.drawArrays(gl.POINTS,0,1);
    </script>
</body>
</html>

二、绘制多点

//如何向attribute变量中写入多点,并绘制多点
//创建顶点数据
const vertices =new Float32Array([0.0,0.1,-0.1,-0.1,0.1,-0.1])
//建立缓存对象,独立缓存区
const vertexBuffer = gl.createBuffer();
//绑定缓存对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//写入数据
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
//获取attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//开启批处理功能
gl.enableVertexAttribArray(a_Position);
//添加顶点的时候,也要修改绘制顶点的数量 
//绘制顶点
 gl.drawArrays(gl.POINTS,0,3);

三、绘制三角面

//不需要顶点大小了
//修改绘图方式就可以了
gl.drawArrays(gl.TRIANGLES,0,3);

四、绘制线

绘制线的方式主要是修改顶点个数和绘图方式gl.drawArrays(mode,first,count);

  • POINTS 可视的点
  • LINES 单独线段
  • LINE_STRIP 线条
  • LINE_LOOP闭合线条
  • TRIANGLES 单独三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇

五、三角带和三角扇的规则

前端面试大全之手写代码一

前端面试大全之手写代码(一)

每日12题(高频基础)

1.构造函数继承

function Super(name,age){
    this.name = name;
    this.age = age;
    this.color = ['red','yellow','blue'];
    this.sayHi = function(){
        console.log('hi')
    }
    console.log(this)
}
function Sub(){
    Super.apply(this,arguments)
    this.height = 180;
}

var instance1 = new Sub('mengfeng',25);
var instance2 = new Sub('mengfeng123',24);
instance1.sayHi();

2.计时器

var i = 10;
let fn = () => {
    console.log(i--);
}
function mySetInterval(fn, delay, times) {
    let timer = setTimeout(function a() {
        fn()
        times--
        timer = setTimeout(a, delay)
        if (times <= 0) {
            clearTimeout(timer)
        }
    }, delay)
}
mySetInterval(fn, 1000, 10)

3.寄生组合继承

 function inheritPrototype(Sub,Super){
    var subPrototype=Object.create(Super.prototype)
    subPrototype.constructor=Sub
    Sub.prototype=subPrototype
    
  }
  function Super(name){
    this.name=name
  }
  Super.prototype.sayHi=function(){
    console.log(this.name)//ccdida
  }
  function Sub(name){
    Super.call(this,name)
  }
  inheritPrototype(Sub,Super)

  Sub.prototype.sayHello=function(){
    console.log('sayHello')
  }

  var instance1=new Sub('ccdida')
  console.log(instance1.__proto__)
  console.log(instance1.__proto__.__proto__)

4.柯里化函数

function add() {
    const _args = [...arguments];
    function fn() {
      _args.push(...arguments);
      return fn; //一直重复收集参数
    }
    fn.toString = function() {
      return _args.reduce((sum, cur) => sum + cur);
    }
    return fn;
  }
console.log(add(1)(2)(3)(4).toString())
console.log(add(1,2)(1, 2, 3)(2).toString())

5.千位符

// 将金额类型转为数字类型
function toNum(str) {
    return str.replace(/\,|\¥/g, "");
}

// 保留两位小数(四舍五入)
function toPrice(num) {
    num = parseFloat(toNum(num)).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");
}

// 保留两位小数(不四舍五入)
function toPrice1(num) {
    num = parseFloat(toNum(num).replace(/(\.\d{2})\d+$/,"$1")).toFixed(2).toString().split(".");
    num[0] = num[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + num.join(".");;
}

// 不处理小数部分
function toPrice2(num) {
    var source = toNum(num).split(".");
    source[0] = source[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)','ig'),"$1,");
    return "¥" + source.join(".")
}

console.log(toPrice('12312.236')) //¥12,312.24
console.log(toPrice1('12312.234')) //¥12,312.23
console.log(toPrice2('1232342312.234')) //¥1,232,342,312.234

6.深拷贝

function deepClone(obj,hash = new WeakMap()){
    if(obj == null) return obj;
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj);
    if(typeof obj !== 'object') return obj;
    if(hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.constructor;
    hash.set(obj,cloneObj);
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            cloneObj[key] = deepClone(obj[key],hash);
        }
    }
    return cloneObj;
}

let obj = {name:1,address:{x:1000}};
let d = deepClone(obj);
obj.address.x = 200;
console.log(d); //{name:1,address:{x:1000}}

7.数组扁平化

//递归实现
var arr = [1,2,[3,4,[5,6]]]
function flatten(arr){
    let result = []
    arr.forEach(item => {
        if(Array.isArray(item)){
            result = result.concat(flatten(item))
        }else{
            result.push(item)
        }
    });
    return result
}
console.log(flatten(arr)) //[ 1, 2, 3, 4, 5, 6 ]

//利用reduce函数迭代
var arr1 = [1,2,[3,4,[5,6]]]
function flatten1(arr){
    return arr.reduce((res,next) => {
        return res.concat(Array.isArray(next) ? flatten1(next) : next)
    },[])
}
console.log(flatten1(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

8.原型链继承

//1.原型链继承
function Super(){
    this.color = ['red','yellow','black']
}
function Sub(){
}

Sub.prototype = new Super();
const instance1 = new Sub();
const instance2 = new Sub();
console.log(instance1.__proto__.color === instance2.__proto__.color)//true

9.组合继承

function Super(name,age){
    this.name = name;
    this.age = age;
    this.color = ['red','yellow','blue']
}

Super.prototype.sayHi = function(){
    console.log('hi')
}

function Sub(name,age,height){
    Super.apply(this,arguments)
    this.height = height;
}

Sub.prototype = new Super('w',22);
Sub.prototype.constructor = Sub;
console.log(Sub.prototype)
Sub.prototype.sayHello = function(){
    console.log('hello')
}

var instance1 = new Sub('mengfeng',23,180);
var instance2 = new Sub('mengfeng123',24,181);
console.log(instance1)

10.instanceof

function new_instance_of(leftValue,rightValue){
    let rightProto = rightValue.prototype;//取右边表达式的 prototype 值
    leftValue = leftValue.__proto__;//取左表达式的 __proto__ 值
    while(true){
        if(leftValue == null){
            return false;
        }
        if(leftValue === rightProto){
            return true;
        }
        leftValue = leftValue.__proto__;
    }
}
function Foo(){}
console.log(new_instance_of(Foo,Object))//true

11.promise.all

Promise.prototype.all = function(promises){
    let results = [];
    let promiseCount = 0;
    let promisesLength = promises.length;
    return new Promise(function(resolve,reject){
        for(let val of promises){
            Promise.resolve(val).then(function(res){
                promiseCount++;
                results[i] = res;
                if(promiseCount === promisesLength){
                    return resolve(results);
                }
            },function(err){
                return reject(err);
            })
        }
    })
}

let promise1 = new Promise(function(resolve) {
    resolve(1);
  });
  let promise2 = new Promise(function(resolve) {
    resolve(2);
  });
  let promise3 = new Promise(function(resolve) {
    resolve(3);
  });
  
  let promiseAll = Promise.all([promise1, promise2, promise3]);
  promiseAll.then(function(res) {
    console.log(res);
  });

12.promise.race

Promise.race = function(promises){
    //将可迭代对象转换为数组
    promises = Array.from(promises);
    return new Promise((resolve,reject)=>{
        if(promises.length === 0){
            //空的可迭代对象,用于pending状态
        }else{
            for(let i = 0;i < promises.length;i++){
                Promise.resolve(promises[i]).then((data)=>{
                    resolve(data);
                }).catch((reason)=>{
                    reject(reason)
                })
            }
        }
    })
}

let p1 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('success')
    },1000)
})

let p2 = new Promise(function(resolve,reject){
    setTimeout(function(){
     resolve('faild')
    },500)
})

Promise.race([p1,p2]).then(result=>{
console.log(result)             //  faild    faild耗时短
})

Drone-UI挂起

Drone-UI挂起

一、问题描述

当我们将.drone.yml配置文件放在项目根目录,执行

git init //初始化仓库
git remote add origin 仓库地址 //链接远程仓库
git add . 
git commit -m "xxxx"
git push 仓库地址 //推送远程仓库

当我们将.drone.yml推送到仓库的时候,那么webhooks就会被触发,我们在drone ci页面就可以看见构建流程

但如果我们的.drone.yml配置失败,那么就会出现一直卡在pending上,或者构建失败

二、修改配置文件

1、.dron.yml文件配置和执行程序对应

如果没有对应配置,那么就会pending

举个荔枝:

#.dron.yml
kind: pipeline
type: docker
name: deployment

platform:
  os: linux
  arch: arm64

steps:
  - name: deployment
    image: node
    commands:
      - pwd
      - ls
      - node app.js

branches: main
//app.js
let res = function(){
    console.log("hello word")
}
res()

三、推送到远程仓库

git add . 
git commit -m "xxxx"
git push 仓库地址 //推送远程仓库

四、查看构建结果

构建成功

我们CI持续集成构建就成功了,我们可以去继续丰富我们配置文件,直到达到我们想要的结果

参考链接:

[Drone入门]: “https://0-8-0.docs.drone.io/zh/getting-started/

宝塔Linux部署

宝塔Linux部署

一、购买云服务器

[阿里云官网]: “https://cn.aliyun.com/?from_alibabacloud=&utm_content=se_1013083955

当我购买了云服务器,那我们就可以登陆我们的服务器去下载mac的宝塔面板(通过外网地址访问浏览器)

二、登陆云服务器

初始化(重置)实例密码

输入Password密码和实例密码

三、安装宝塔

然后可以看到已经进了服务器,那么我们就可以开始安装了

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh fa1f2600

稍等片刻后,输入“y”继续

下载成功之后,就会出现账户和密码和访问地址

我们通过外网面板地址访问

如果现在无法访问的话,就去阿里云服务器实例安全组添加提示端口

现在应该可以访问了,如果还不行就添加8888端口

四、配置宝塔

当我们访问成功,根据提示安装宝塔面板所需要的创建就可以了

如果我们下载插件时间过长,重启宝塔面板

五、创建站点

我们来创建我们第一个站点

提交的时候我们可能遇见下面的提示,配置错误

nginx: [emerg] open() "/www/server/nginx/conf/enable-php.conf" failed (2: No such file or directory) in /www/server/nginx/conf/nginx.conf:79 nginx: configuration file /www/server/nginx/conf/nginx.conf test failed

有一个最简单的方案就是,替换nginx 的版本,换一个更低一点的版本

重启宝塔面板就可以了

六、上传站点部署文件

选择要上传文件的站点

选择要上传的文件,可以拖拽

那么我们现在访问网站就可以了

如果还是访问不到的话(阿里云购买的域名),建议先进行备案,然后再部署网站

Dokcer部署Drone2

Dokcer部署Drone2

这一篇延续上一篇的问题,这一篇解决仓库初始化失败(500)的问题

一、问题原因

问题在于 drone 创建 webhooks 时失败,Github 不允许创建本地 localhost 类型的 webhooks,因为这样执行时是在本地,没办法定位到具体的用户主机。可以使用 ngrok 这样的工具把本地服务绑定到公网。

二、解决问题

1.安装ngrok

安装ngrok的时候,mac用户默认下载的位置和终端的位置不一样,所以我们统一安装到 /usr/local/bin 目录

具体步骤是:

步骤一:解压缩文件后复制ngrok

步骤二:前往文件夹

步骤三:复制ngrok,双击执行

进程完成

2.连接服务

执行ngrok的密钥,连接服务

ngrok config add-authtoken 2HW491ApRpQILMM3kwW_4T7x8zVrAXpqUEjdSGU4y

当出现下面的情况,就说明安装成功了,就可以使用命令了

如果提示没有验证邮箱,就先去验证邮箱,要不然服务会中断

3.开启服务端口

ngrok http 80

4.修改Drone配置(github)

#Homepage URL
https://8313-183-202-88-243.ap.ngrok.io

5.修改docker-compose.yml 和 .env 文件

vim docker-compose.yml  //配置文件
vim .env
//docker-compose.yml
version: '3.7'

services:

  drone-server:
    image: drone/drone:2.3.1
    ports:
      - 80:80
    volumes:
      - drone-data:/data:rw
      - ./ssl:/etc/certs
    restart: always
    environment:
      - DRONE_SERVER_HOST=${DRONE_SERVER_HOST:-https://drone.yeasy.com}
      - DRONE_SERVER_PROTO=${DRONE_SERVER_PROTO:-https}
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET:-secret}
      - DRONE_GITHUB_SERVER=https://github.com
      - DRONE_GITHUB_CLIENT_ID=${DRONE_GITHUB_CLIENT_ID}
      - DRONE_GITHUB_CLIENT_SECRET=${DRONE_GITHUB_CLIENT_SECRET}

  drone-runner:
    image: drone/drone-runner-docker:1
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:rw
    environment:
      - DRONE_RPC_PROTO=http
      - DRONE_RPC_HOST=drone-server
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET:-secret}
      - DRONE_RUNNER_NAME=${HOSTNAME:-demo}
      - DRONE_RUNNER_CAPACITY=2
    dns: 114.114.114.114

volumes:
  drone-data:
//.env
# 必填 服务器地址,例如 drone.domain.com
DRONE_SERVER_HOST=8313-183-202-88-243.ap.ngrok.io
DRONE_SERVER_PROTO=https
DRONE_RPC_SECRET=24c08d3d73e59c5d9dd5e26e3256c1f9
HOSTNAME=demo
# 必填 在 GitHub 应用页面查看
DRONE_GITHUB_CLIENT_ID=
# 必填 在 GitHub 应用页面查看
DRONE_GITHUB_CLIENT_SECRET=

5.重启Drone

docker-compose up -d

这样我们就解决了500的问题,github上的Webhooks就启动了

  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信