<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        chenwl

        chenwl 查看完整檔案

        廣州編輯  |  填寫畢業院校  |  填寫所在公司/組織 youyoucuocuo.top 編輯
        編輯

        平坦的路面上曲折前行

        個人動態

        chenwl 發布了文章 · 3月25日

        基于vue3的小型圖書管理項目

        前言

        Vue3 練手項目,為了加深對 composition-api 的理解,項目參考于 sl1673495/vue-bookshelf,不過這個項目還是基于 vue2+composition-api,里面對于組合函數的使用和理解還是很有幫助的,這里用 Vue3 做了修改。

        項目地址:vue-bookshelf

        項目中會用到的 Vue3 api,你需要在開始之前對它們有所了解:

        • [x] Provide / Inject
        • [x] ref、reactive、watch、computed
        • [x] directive
        • [x] 生命周期函數
        • [x] v-model 多選項綁定

        provide/inject代替vuex

        Vue3 中新增的一對api,provideinject,可以很方便的管理應用的全局狀態,有興趣可以參考下這篇文章:Vue 3 store without Vuex

        官方文檔對 Provide / Inject 的使用說明:Provide / Inject

        利用這兩個api,在沒有vuex的情況下也可以很好的管理項目中的全局狀態:

        import { provide, inject } from 'vue'
        
        const ThemeSymbol = Symbol()
        
        const Ancestor = {
          setup() {
            provide(ThemeSymbol, 'dark')
          }
        }
        
        const Descendent = {
          setup() {
            const theme = inject(ThemeSymbol, 'light' /* optional default value */)
            return {
              theme
            }
          }
        }

        開始

        項目介紹

        項目很簡單,主要邏輯如下:

        • 加載圖書列表數據
        • 路由頁:未閱圖書列表/已閱圖書列表
        • 功能:設置圖書已閱、刪除圖書已閱

        項目搭建

        項目基于 vue-cli 搭建:

        • typescript
        • vue3
        • vue-router
        • sass

        context

        項目基于 Provide/Inject 實現全局的圖書狀態管理,context/books.ts包含兩個組合函數:

        • useBookListProvide 提供書籍的全局狀態管理和方法
        • useBookListInject 書籍狀態和方法注入(在需要的組件中使用)

        在main.ts中,根組件注入全局狀態:

        // main.ts
        import { createApp, h } from 'vue'
        import App from './App.vue'
        import { useBookListProvide } from '@/context'
        
        const app = createApp({
          setup() {
            useBookListProvide();
            return () => h(App)
          }
        })

        組件中使用:

        import { defineComponent } from "vue";
        import { useBookListInject } from "@/context";
        import { useAsync } from "@/hooks";
        import { getBooks } from "@/hacks/fetch";
        
        export default defineComponent({
          name: "books",
          setup() {
          // 注入全局狀態
            const { setBooks, booksAvaluable } = useBookListInject();
            
         // 獲取數據的異步組合函數
            const loading = useAsync(async () => {
              const requestBooks = await getBooks();
              setBooks(requestBooks);
            });
        
            return {
              booksAvaluable,
              loading,
            };
          }
        });

        組合函數 useAsync 目的是管理異步方法前后loading狀態:

        import { onMounted, ref } from 'vue'
        
        export const useAsync = (func: () => Promise<any>) => {
          const loading = ref(false)
          onMounted(async () => {
            try {
              loading.value = true
              await func()
            } catch (error) {
              throw error
            } finally {
              loading.value = false
            }
          })
        
          return loading
        }

        組件中使用:

        <Books :books="booksAvaluable" :loading="loading"></Books>

        分頁

        對于分頁這里使用組合函數 usePages 進行管理,目的是返回當前頁的圖書列表和分頁組件所需的參數:

        import { reactive, Ref, ref, watch } from 'vue'
        
        export interface PageOption {
          pageSize?: number
        }
        
        export function usePages<T>(watchCallback: () => T[], pageOption?: PageOption) {
          const { pageSize = 10 } = pageOption || {}
        
          const rawData = ref([]) as Ref<T[]>
          const data = ref([]) as Ref<T[]>
        
          const bindings = reactive({
            current: 1,
            currentChange: (currentPage: number) => {
              data.value = sliceData(rawData.value, currentPage)
            },
          })
        
          const sliceData = (rawData: T[], currentPage: number) => {
            return rawData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
          }
        
          watch(
            watchCallback,
            (value) => {      
              rawData.value = value
              bindings.currentChange(1)
            },
            {
              immediate: true,
            }
          )
        
          return {
            data,
            bindings,
          }
        }

        基于 composition-api 可以很方便的將統一的邏輯進行拆分,例如分頁塊的邏輯,很可能在其它的業務模塊中使用,所以統一拆分到了hooks文件夾下。

        這里簡單實現了分頁插件,參考 element-plus/pagination 的分頁組件。

        <Pagination
          class="pagination"
          :total="books.length"
          :page-size="pageSize"
          :hide-on-single-page="true"
          v-model:current-page="bindings.current"
          @current-change="bindings.currentChange"
        />

        Vue3 可以實現在組件上使用多個 v-model 進行雙向數據綁定,讓 v-model 的使用更加靈活,詳情可查看官方文檔 v-model。

        項目中的分頁組件也使用了v-model:current-page 的方式進行傳參。

        圖片加載指令

        vue3 的指令也做了更新: 官方文檔-directives

        主要是生命周期函數的變化:

        const MyDirective = {
          beforeMount(el, binding, vnode, prevVnode) {},
          mounted() {},
          beforeUpdate() {}, // new
          updated() {},
          beforeUnmount() {}, // new
          unmounted() {}
        }

        項目中的指令主要是針對圖片src做處理,directives/load-img-src.ts

        // 圖片加載指令,使用 ![](默認路徑)
        
        // 圖片加載失敗路徑
        const errorURL =
          'https://imgservices-1252317822.image.myqcloud.com/image/20201015/45prvdakqe.svg'
        
        const loadImgSrc = {
          beforeMount(el: HTMLImageElement, binding: { value: string }) {
            const imgURL = binding.value || ''
            const img = new Image()
            img.src = imgURL
            img.onload = () => {
              if (img.complete) {
                el.src = imgURL
              }
            }
            img.onerror = () => (el.src = errorURL)
          },
        }
        查看原文

        贊 3 收藏 1 評論 1

        chenwl 發布了文章 · 3月19日

        vue3響應式原理

        vue3 響應式原理的實現,源碼地址:vue3_reactivity

        rollup 搭建ts環境

        安裝 rollup 插件

        npm install rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-replace rollup-plugin-serve typescript -D
        包名功能
        rollup打包工具
        rollup-plugin-typescript2解析ts插件
        @rollup/plugin-node-resolve解析第三方模塊
        @rollup/plugin-replace替換插件
        rollup-plugin-serve啟動本地服務插件

        配置打包環境

        生成 tsconfig.json 文件:

        npx tsx --init

        修改 tsconfig.json 配置屬性 moduleESNext(默認在瀏覽器運行)

        可以設置 strict 屬性為false,讓 ts 支持 any 類型,scouceMap 需要設置成 true,方便調試代碼

        根目錄新建 rollup.config.js 配置文件,并輸入下面內容:

        import path from "path";
        import ts from "rollup-plugin-typescript2";
        import { nodeResolve } from "@rollup/plugin-node-resolve";
        import replace from "@rollup/plugin-replace";
        import serve from "rollup-plugin-serve";
        
        export default {
          input: 'src/index.ts',
          output: {
            name: 'VueReactivity',
            format: 'umd',
            file: path.resolve('dist/VueReactivity.js'),
            sourcemap: true,
          },
          plugins: [
            nodeResolve({
              extensions: ['.js', '.ts'],
            }),
            ts({
              tsconfig: path.resolve(__dirname, 'tsconfig.json'),
            }),
            replace({
                "process.env.NODE_ENV": JSON.stringify("development"),
            }),
            serve({
                open: true,
                openPage: "/public/index.html",
                port: 3000,
                contentBase: ""
            })
          ],
        }

        新建入口文件srx/index.ts和模板文件public/index.html。

        模板文件 index.html 需要手動引入打包后的 /dist/vue.js

        package.json 添加打包命令:

        "scripts": {
          "dev": "rollup -c -w"
        }

        reactivity模塊

        先看看Vue的reactivity模塊實現效果,先安裝 reactivity:

        npm install @vue/reactivity

        測試 public/index.html 內容:

        <div id="app"></div>
        <script data-original="/node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
        <script>
            const {reactive, effect} = VueReactivity;
            const state = reactive({name:"chenwl",age:12,address:"guangzhou1"});
            effect(()=>{
                app.innerHTML = `${state.name} 今年 ${state.age} 歲`
            });
            
            // 當 effect 函數中依賴的數據發生變化 effect 會重新執行
            setTimeout(() => {
                state.name = "change"
            }, 1000);
        </script>

        核心:當讀取文件時,做依賴收集,當數據變化時重新執行effect

        初始化目錄結構

        -src
         - reactivity
             - effect.ts
             - reactive.ts
             - index.ts
         - shared
             - index.ts //通用方法
         - index.ts

        reactivity/index.ts

        export { reactive } from './reactive'
        export { effect } from './effect'

        src/index.ts

        export * from "./reactivity/index"

        創建響應式對象

        reactive/reactive.ts

        import { isObject } from "../shared/index"
        
        const mutableHandlers = {
            get(){},
            set(){}
        }
        
        export function reactive(target){
            // 將目標對象變成響應式對象,Proxy
            return createReactiveObject(target, mutableHandlers)
        }
        
        // 核心:當讀取文件時,做依賴收集,當數據變化時重新執行effect
        function createReactiveObject(target, baseHandlers) {
           // 如果是不是對象,直接返回
          if(!isObject(target)) return target;
            
          return new Proxy(target, baseHandlers)
        }

        簡單的實現下 isObject 方法:

        export const isObject = (val) => typeof val === 'object' && val !== null

        Proxy優勢:

        • Proxy 直接監聽對象而非屬性,只是對外層對象做代理,默認不會遞歸,不會重寫對象中的屬性
        • Proxy 可以直接監聽數組的變化
        • Proxy 返回的是一個新對象,我們可以只操作新的對象達到目的,而 Object.defineProperty 只能遍歷對象屬性直接修改

        創建映射表

        為防止對象被重復代理,這里使用WeakMap創建代理元素的映射表,如果對象被代理過,則直接返回:

        // 映射表
        const proxyMap = new WeakMap()
        
        function createReactiveObject(target, baseHandlers) {
          if (!isObject(target)) return target
          
          // 從映射表中取出,判斷是否被代理過
          const existingProxy = proxyMap.get(target)
          if (existingProxy) return existingProxy
        
          const proxy = new Proxy(target, baseHandlers)
          // 放入代理對象
          proxyMap.set(target, proxy)
        
          return proxy
        }
        WeakMap 相對于 Map 也是鍵值對集合,但是 WeakMap 的key 只能是非空對象(non-null object),WeakMap 對它的 key 僅保持弱引用,也就是說它不阻止垃圾回收器回收它所引用的 key,WeakMap 最大的好處是可以避免內存泄漏。一個僅被 WeakMap 作為 key 而引用的對象,會被垃圾回收器回收掉。

        代理工廠函數

        為了方便管理代理邏輯,這里拆分 mutableHandlers 對象到新文件/reactivity/haseHandler.ts 中。

        reactivity/haseHandler.ts

        function createGetter() {
          return function get(target, key, reaciver) {}
        }
        
        function createSetter() {
          return function set(target, key, value, receiver) {}
        }
        
        const get = createGetter();
        const set = createSetter()
        
        export const mutableHandlers = {
          get,  // 獲取對象會執行此方法
          set, // 設置屬性值會執行此方法
        }
        set 和 get 方法通過工廠函數創建,工廠函數的可以方便參數的傳參和預置

        reactive.ts 文件中引入:

        import { mutableHandlers } from './baseHandler'
        
        export function reactive(target) {
          // 將目標對象變成響應式對象,Proxy
          return createReactiveObject(target, mutableHandlers)
        }

        getter 方法

        當代理對象的屬性被獲取時:

        function createGetter() {
          return function get(target, key, reaciver) {
            const res = Reflect.get(target, key, reaciver) // 相當于 target[key];
            
            // 不對 symbol 類型做處理
            if (typeof key === 'symbol') return res;
        
            console.log('此時代理對象的屬性被獲取')
            
            // 如果是對象,進行遞歸代理
            if (isObject(res)) return reactive(res);
        
            return res
          }
        }

        setter 方法

        在對屬性進行設置之前,需要判斷是修改值還是新增值,并且需要注意,如果是數組,需要判斷修改的方式是否通過索引添加:

        let arr = [1];
        arr[10] = 10; // 通過索引新增值的數組

        所以判斷之前,還需要對target進行判斷,如果是數組,需要增加索引判讀。

        數組索引比原數組長度小 ? 修改 : 新增

        通過target[key]先獲取舊值,然后再跟新值比對判斷。

        代碼邏輯:

        function createSetter() {
          return function set(target, key, value, receiver) {
            const oldValue = target[key] // 獲取舊值,看下有沒有這個屬性
            
            // 如果是數組,根據索引判斷是修改還是新增
            const hasKey =
              isArray(target) && isInteger(key)
                ? Number(key) < target.length
                : hasOwn(target, key)
        
            const result = Reflect.set(target, key, value, receiver);
            
            if(!hasKey){
                console.log("新增屬性");
            }else if (hasChanged(value, oldValue)) {
              console.log('修改屬性')
            }
            
            return result
          }
        }

        通用方法:

        export const isArray = Array.isArray
        
        export const isInteger = (key) => '' + parseInt(key, 10) === key
        
        const hasOwnProperty = Object.prototype.hasOwnProperty;
        export const hasOwn = (val, key) => hasOwnProperty.call(val,key);
        
        export const hasChanged = (value, oldValue) => value !== oldValue

        修改模板 public/index.html 下的引用,可以看到控制臺輸出對應的屬性設置操作。

        <script data-original="../dist/VueReactivity.js"></script>
        <script>
          const { reactive, effect } = VueReactivity
          const state = reactive({ name: 'chenwl', age: 12, address: 'guangzhou1' })
        
          state.name = 'change' // 修改屬性
          state.newProp = 'newProp' // 新增屬性
        
          const stateArr = reactive(['a', 'b', 'c'])
          stateArr[0] = 'array change' // 數組修改
          stateArr[3] = 'add array' // 數組新增
        </script>

        effect 函數

        回到開始寫的 public/index.html 內容:

        <div id="app"></div>
        <script data-original="../dist/VueReactivity.js"></script>
        <script>
            const { reactive, effect } = VueReactivity
            const state = reactive({ name: 'chenwl', age: 12 })
            effect(()=>{
                app.innerHTML = `${state.name} 今年 ${state.age} 歲`
            });
        
            setTimeout(() => {
                state.name = 'change' // 修改屬性
            }, 1000)
        </script>

        頁面初始化后,app 標簽的內容為 chenwl 今年 12 歲,一秒后修改為:change 今年 12 歲。

        當代理對象的值發生改變時,effect函數參數里面用戶自定義的方法也會執行

        初始化 effect 函數

        上面的邏輯可以得到,effect 方法第一個參數為用戶自定義的方法,里面存放用戶自己的邏輯,這個方法在下面的情況下會執行:

        • options.lazy 為 false,初始化時執行
        • 出現在自定義函數里面的代理對象發生改變

        修改 effect.ts 如下:

        export function effect(fn, options: any = {}) {
          const effect = createReactiveEffect(fn, options)
        
          if (!options.lazy) {
            effect()
          }
        
          return effect
        }
        
        function createReactiveEffect(fn, options) {
          const effect = function () {
            return fn() // 用戶自己寫的邏輯,內部會對數據進行取值操作
          }
        
          return effect
        }

        收集依賴

        聲明變量 activeEffect 存儲當前執行的 effect 函數:

        let activeEffect; // 用來存儲當前的effect函數
        function createReactiveEffect(fn, options) {
          const effect = function () {
            activeEffect = effect
            return fn()
          }
          return effect
        }
        fn 函數執行時,函數上下文的響應式變量會做取值(getter)操作,此時可以通過activeEffect獲取當前響應式變量關聯的effect
        // fn函數執行,觸發響應式變量`state`的取值操作
        effect(() => {
          app.innerHTML = `${state.name} 今年 ${state.age} 歲`
        })
        
        ...
        
        // baseHandler.ts
        function createGetter(){
            return function get(target, key, reaciver) {
                // 觸發取值操作
            }
        }

        track依賴收集

        為了將響應式屬性和effect進行關聯,這里聲明 track 函數進行依賴收集:

        // effect.ts
        export function track(target,key){
            if(activeEffect === undefined) return;
        }

        當調用fn()時,會執行用戶傳入的函數,此時會進行取值操作,我們在這里實現依賴收集功能:

        // baseHandler.ts 
        function createGetter() {
          return function get(target, key, reaciver) {
            console.log('此時代理對象的屬性被獲取')
            track(target, key)
          }
        }

        建立映射表,存儲 effect 更新函數 和 響應式屬性 的關系:

        // 映射表
        const targetMap = new WeakMap(); 
        // targetMap = {target:{key:[effect,effect]}}
        // 屬性和effect關聯
        export function track(target, key) {
          if (!activeEffect) return
        
          let depsMap = targetMap.get(target)
          if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()))
          }
          let dep = depsMap.get(key)
          if (!dep) {
            depsMap.set(key, (dep = new Set()))
          }
          if (!dep.has(activeEffect)) {
            dep.add(activeEffect)
            activeEffect.deps.push(dep)
          }
        }
         let activeEffect; // 用來存儲當前的effect函數
        +let uid = 0;
        function createReactiveEffect(fn, options) {
          const effect = function () {
              activeEffect = effect
              return fn()
          }
        +  effect.id = uid++ // effect標識
        +  effect.deps = [] // 用來表示 effect 中依賴了哪些屬性
        +  effect.options = options // effect中參數
        
          return effect
        }

        清空 activeEffect

        當依賴收集完成,需要清空當前的 activeEffect 方法:

        function createReactiveEffect(fn, options) {
          const effect = function () {
        +    try {
              activeEffect = effect
              return fn()
        +    } finally {
        +      activeEffect = null
        +    }
          }
          ...
          return effect
        }
        
        export function track(target, key) {
          if (!activeEffect) return; // 不存在或被清空不執行映射關系存儲
        }

        但是如果出現下面的情況:

        effect(()=>{
            state.name;
            effect(()=>{
                state.age
            });
            state.address
        })

        內部的effect方法在收集完依賴后,就會清空activeEffect方法,導致最后的state.address 沒有被收集。

        棧結構清空,保證清空的是最后一個effect

         let activeEffect
         let uid = 0
        + const effectStack = []
        function createReactiveEffect(fn, options) {
          const effect = function () {
            try {
              activeEffect = effect
        +      effectStack.push(activeEffect)
              return fn()
            } finally {
        +      effectStack.pop()
        +      activeEffect = effectStack[effectStack.length - 1]
            }
          }
          effect.id = uid++
          effect.deps = []
          effect.options = options
        
          return effect
        }

        處理死循環:

        effect(() => {
          state.age++
          app.innerHTML = `${state.name} 今年 ${state.age} 歲`
        })

        state.age一直在變化會導致effect不斷的遞歸執行,為防止這種情況,如果effectStack存儲了同樣的effect略過:

        const effectStack = []
        function createReactiveEffect(fn, options) {
          const effect = function () {
        +    if (effectStack.includes(effect)) return
            try {} finally {}
          }
          ...
        }

        trigger觸發更新

        依賴收集后,接下來觸發函數更新,這里實現trigger函數觸發更新:

        export enum TriggerType {
          add = 'add',
          set = 'set',
        }
        
        export function trigger(target, type:TriggerType, key, value?, oldValue?) {
          const depsMap = targetMap.get(target)
          if (!depsMap) return
        
          const run = (effects) => {
            if (effects) effects.forEach((effect) => effect())
          }
        
          if (key != void 0) {
            run(depsMap.get(key))
          }
        }

        設置響應式屬性時,觸發 trigger

        function createSetter() {
          return function set(target, key, value, receiver) {
            const oldValue = target[key] 
            ...
            if (!hasKey) {
              // 新增屬性
        +      trigger(target, TriggerType.add, key, value)
            } else if (hasChanged(value, oldValue)) {
              // 修改屬性
        +      trigger(target, TriggerType.set, key, value, oldValue)
            }
            ...
          }
        }

        數組觸發的更新

        情況一:收集和修改都是數組屬性(length)

        const state = reactive([1, 2, 3])
        effect(() => {
          app.innerHTML = state.length
        })
        setTimeout(() => {
          state.length = 100
        }, 1000)

        結果:觸發更新

        情況二:修改數組長度,沒有收集數組屬性

        const state = reactive([1, 2, 3])
        effect(() => {
          app.innerHTML = state[2]
        })
        setTimeout(() => {
          state.length = 1
        }, 1000)

        結果:屬性修改,沒有觸發更新

        修改條件判斷:

        if (key === 'length' && isArray(target)) {
          depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= value) {
              run(dep)
            }
          })
        } else {
          if (key != void 0) {
            run(depsMap.get(key))
          }
        }

        情況三:通過索引增加數組選項,收集數組長度小于修改長度

        const state = reactive([1, 2, 3])
        effect(() => {
          app.innerHTML = state
        })
        setTimeout(() => {
          state[10] = 10
        }, 1000)

        結果:通過索引修改,沒有觸發更新

        添加條件判斷:

        switch (type) {
          case 'add':
            if (isArray(target)) {
              // 數組通過索引增加選項
              if (isInteger(key)) {
                run(depsMap.get('length'))
              }
            }
        }

        完整的 trigger 函數:

        export enum TriggerType {
          add = 'add',
          set = 'set',
        }
        
        export function trigger(target, type: TriggerType, key, value?, oldValue?) {
          const depsMap = targetMap.get(target)
          if (!depsMap) return
        
          const run = (effects) => {
            if (effects) effects.forEach((effect) => effect())
          }
        
          if (key === 'length' && isArray(target)) {
            depsMap.forEach((dep, key) => {
              if (key === 'length' || key >= value) {
                run(dep)
              }
            })
          } else {
            if (key != void 0) {
              run(depsMap.get(key))
            }
            switch (type) {
              case 'add':
                if (isArray(target)) {
                  // 數組通過索引增加選項
                  if (isInteger(key)) {
                    run(depsMap.get('length'))
                  }
                }
            }
          }
        }

        響應式過程

        通過下面的例子來回顧下vue3響應式執行的過程

        const { reactive, effect } = VueReactivity
        
        // reactive 方法將參數變成響應式對象
        const state = reactive({ name: 'chenwl' })
        
        // effect 內部如何操作
        effect(() => {
          app.innerHTML = `姓名:${state.name}`
        })
        
        setTimeout(() => {
          state.name = 'change'
        }, 1000)

        首先 reactive 將參數變成響應式對象并返回,接著就是effect函數的執行過程

        let activeEffect;
        const effect = function (fn){
            console.log("1、effect 函數執行");
            try{
                console.log("2、保存當前effect函數到 activeEffect");
                activeEffect = effect;
                console.log("3、fn 函數執行");
                fn()
            }finally{
                // 清空 activeEffect
            }
        }

        fn() 函數執行,state.name 作為響應式屬性會進入它的getter訪問器:

        function createGetter() {
          return function get(target, key, reaciver) {
            const res = Reflect.get(target, key, reaciver) // 相當于 target[key];
        
            if (typeof key === 'symbol') return res // 不對 symbol 類型做處理
        
            console.log(`4、進入 ${key} => getter 訪問器`)
            track(target, key)
        
            if (isObject(res)) return reactive(res) // 如果是對象,進行遞歸代理
        
            return res
          }
        }

        getter 訪問器里面,track 會收集當前屬性所依賴的effect函數:

        const targetMap = new WeakMap()
        export function track(target, key) {
          if (activeEffect == undefined) return
        
          let depsMap = targetMap.get(target)
          if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()))
          }
          let dep = depsMap.get(key)
          if (!dep) {
            depsMap.set(key, (dep = new Set()))
          }
          if (!dep.has(activeEffect)) {
            dep.add(activeEffect)
            activeEffect.deps.push(dep)
          }
          // targetMap = { target: { key: [effect, effect] } }
          console.log(`5、${key} => 收集依賴:`, targetMap)
        }

        state.name 發生修改操作,進入到響應式屬性的設置方法并觸發trigger更新方法:

        function createSetter() {
          return function set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver)
            
            console.log(`6、${key} => 修改屬性`)
            trigger(target, TriggerType.set, key, value, oldValue)
            
            return result
          }
        }

        trigger方法里面找到targetMap對應的target.key,獲取當前響應式屬性所在的effect函數并執行更新操作

        export function trigger(target, type: TriggerType, key, value?, oldValue?) {
        
          console.log(`8、${key} => 觸發更新`)
        
          const run = (effects=[]) => {
            effects.forEach(effect=>{
                console.log(`9、獲取${key} => targetMap的effect執行`)
                console.log('===== 進入key存儲的effect =====')
                effect();
            })
          }
        
          if (key != void 0) {
            run(depsMap.get(key))
          }
         
        }

        控制臺打印結果:

        1、effect
        2、保存當前effect函數到activeEffect
        3、fn函數執行
        4、進入 name => getter 訪問器
        5、name => 收集依賴: WeakMap?{{…} => Map(1)}
        6、name => 修改屬性
        7、name => 觸發更新
        8、獲取name => targetMap的effect執行
        ===== 進入key存儲的effect =====
        1、effect
        2、保存當前effect函數到activeEffect
        3、fn函數執行
        4、進入 name => getter 訪問器
        5、name => 收集依賴: WeakMap?{{…} => Map(1)}

        計算屬性 Computed

        computed 使用

        計算屬性 computed 的使用:

        <div id="app"></div>
        <script data-original="/node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
        <script>
          const { reactive, effect, computed } = VueReactivity
        
          const state = reactive({ name: 'chenwl', age: 12 })
          const birth_year = computed(() => {
            return new Date().getFullYear() - state.age
          })
        
          effect(() => {
            app.innerHTML = `${state.name} 出生于 ${birth_year.value} 年`
          })
        
          setTimeout(() => {
            state.age++
          }, 1000)
        </script>

        state.age 的值發生變化時,依賴于它的 birth_year 會重新執行計算屬性。

        computed 返回值

        通過打印 birth_year 可以在控制臺看到它的值:

        ComputedRefImpl = {
          __v_isReadonly: true,
          __v_isRef: true,
          _dirty: true,
          setter: ?,
          effect: ?,
          _value: 2008,
          value: 2008,
        }
        默認計算屬性的值被包裝到了value屬性上

        初始化 computed

        新建 reactivity/computed.ts 并導出:

        export function computed(getterOrOptions) {
          let getter
          let setter
        
          if (isFunction(getterOrOptions)) {
            getter = getterOrOptions
            setter = () => console.warn('computed not set value')
          } else {
            getter = getterOrOptions.get
            setter = getterOrOptions.set
          }
        }

        computed 接收一個參數,這個參數可能是函數也可能是 {getter,setter} 對象,初始化函數并對參數進行判斷。

        ComputedRefImpl 類

        通過上面 birth_year 的打印結果,可以發現計算屬性返回的是一個 ComputedRefImpl 實例,所以聲明 ComputedRefImpl 類:

        import { effect } from './effect.ts'
        
        class ComputedRefImpl {
          public effect
          constructor(getter, setter) {
            // 默認 getter 執行時會依賴于 effect(計算屬性默認是effect)
            this.effect = effect(getter, {
              lazy: true, // 默認初始化不執行
            })
          }
        }
        
        export function computed(getterOrOptions) {
          let getter
          let setter
        
          if (isFunction(getterOrOptions)) {
            getter = getterOrOptions
            setter = () => console.log('computed not set value')
          } else {
            getter = getterOrOptions.get
            setter = getterOrOptions.set
          }
        
          return new ComputedRefImpl(getter, setter)
        }

        聲明 ComputedRefImpl 類的公共屬性和value屬性的訪問器 get 和設置 set:

        import { effect, track } from './effect.ts'
        
        class ComputedRefImpl {
          public effect
          public __v_isReadonly = true
          public __v_isRef = true // 判斷是否直接返回 value 值
          public _dirty = true // 緩存數據
          private _value
          constructor(getter, public setter) {
            // 默認 getter 執行時會依賴于 effect(計算屬性默認是effect)
            this.effect = effect(getter, {
              lazy: true,
            })
          }
          get value() {
            // 當計算屬性執行時,收集計算屬性的 effect
            this._value = this.effect()
            return this._value
          }
          set value(newValue) {
            this.setter(newValue)
          }
        }

        計算屬性的依賴收集和scheduler

        分析

        計算屬性里面的響應式屬性一旦被修改,需要通知計算屬性所在的effect函數做出更新操作:

        如下,計算屬性 birth_year 里面包含響應式屬性 state.age:

        const birth_year = computed(()=>{
            return new Date().getFullYear() - state.age
        })

        state.age做出修改操作:

        setTimeout(() => {
            state.age++
        }, 1000);

        通知birth_year所在的effect函數做出更新操作:

        effect(() => {
          app.innerHTML = `出生于 ${birth_year.value} 年`
        })

        邏輯實現:

        1、首先第一個 effect 方法開始執行,產生 activeEffect 并存儲在 stackEffects 數組中:

        const stackEffects = [activeEffect]

        這里的 activeEffect 等于下面的方法:

        effect1 = () => {
          app.innerHTML = `出生于 ${birth_year.value} 年`
        }

        也就是:

        const stackEffects = [effect1]

        2、接下來會進入計算屬性 birth_year 的訪問器 value 方法,需要返回計算屬性的執行結果:

        get value(){
            return new Date().getFullYear() - state.age
        }

        為了記錄當前計算屬性所依賴的 effect 函數,修改ComputedRefImpl如下:

        private _value;
        constructor(getter, public setter) {
            this.effect = effect(getter, {lazy: true})
        }
        get value() {
            this._value = this.effect()
            return this._value
        }

        effect 方法的執行存儲了當前計算屬性所在的 activeEffect,現在stackEffects 數組保存了兩個 activeEffect:

        const stackEffects = [effect1,effect2]

        effect2實際上是計算屬性的方法:

        effect2 = ()=>{
         return new Date().getFullYear() - state.age
        }
        第一個 activeEffect 來自更新內容的 effect 函數,第二個 activeEffect 來自 computed

        3、this.effect() 方法執行后,進入到state.age屬性訪問器進行依賴收集,這里通過targetMap 映射表會將state.ageactiveEffect 進行關聯:

        targetMap = {state: { age: effect2 } }

        4、關聯后的 state.age 的發生更新操作,觸發 effect2 函數的重新執行,下面是 effect1effect2 對應的函數:

        effect1 = () => app.innerHTML = `出生于 ${birth_year.value} 年`;
        effect2 = ()=> new Date().getFullYear() - state.age;

        5、計算屬性期望的是 state.age 的更新能夠觸發 effect1 的重新執行,所以在獲取計算屬性時,需要進行依賴收集:

        get value() {
            this._value = this.effect()
        +    track(this, 'value')
            return this._value
        }

        track 的執行讓 targetMap 里面映射表變成了下面這樣:

        const targetMap = {
          state: { age: effect2 },
          ComputedRefImpl: { value: effect1 }
        }

        6、為了讓 state.age 的更新能夠觸發 effect1 的重新執行,修改構effect的options選項,新增scheduler 方法:

        constructor(getter, public setter) {
            this.effect = effect(getter, {
                lazy: true, // lazy=true 默認不會執行
                scheduler: () => {
                    trigger(this, TriggerType.set, 'value')
                },
            })
        }

        修改effect.ts里面的 trigger 方法:

        function trigger(){
        ...
          const run = (effects=[]) => {
            effects.forEach((effect) => {
                if(effect.options.scheduler){
                    effect.options.scheduler(effect)
                }else{
                    effect();
                }
            })
          }
         ...
        }

        effectscheduler 屬性方法時,執行 scheduler 方法,也就是 state.age 的修改會執行下面的邏輯:

        trigger(this, TriggerType.set, 'value')

        這個觸發更新等于觸發了 effect1 方法的重新執行:

        effect1 = () => app.innerHTML = `出生于 ${birth_year.value} 年`;

        執行過程

        computed 的執行:

        1、state.age 更新觸發了 birth_year 的 computed effect 函數
        2、computed effect 執行計算屬性的 scheduler 方法
        3、scheduler 觸發了 birth_year.value 所在的 effect 函數更新

        state.age => computed effect => scheduler => effect函數(birth_year.value)

        完整的 computed 方法:

        import { effect, track, trigger, TriggerType } from './effect.ts'
        
        class ComputedRefImpl {
          public effect
          public __v_isReadonly = true
          public __v_isRef = true // 判斷是否直接返回 value 值
          public _dirty = true // 緩存數據
          private _value
          constructor(getter, public setter) {
            // 默認 getter 執行時會依賴于 effect(計算屬性默認是effect)
            this.effect = effect(getter, {
              lazy: true,
        +      scheduler: () => {
        +        trigger(this, TriggerType.set, 'value')
        +      },
            })
          }
          get value() {
            // 當計算屬性執行時,收集計算屬性的 effect
            this._value = this.effect()
            // 收集計算屬性里面的依賴
        +    track(this, 'value')
            return this._value
          }
          set value(newValue) {
            this.setter(newValue)
          }
        }

        依賴緩存

        當修改跟計算屬性沒有關聯的state.name時,可以看到birth_yeareffect也會被執行:

        const { reactive, effect, computed } = VueReactivity
        
        const state = reactive({ name: 'chenwl', age: 12 })
        const birth_year = computed(() => {
        +  console.log('computed execute')
          return new Date().getFullYear() - state.age
        })
        
        effect(() => {
          app.innerHTML = `${state.name} 出生于 ${birth_year.value} 年`
        })
        
        setTimeout(() => {
        +  state.name = 'change'
        }, 1000)

        state.name發生改變,控制臺打印出 'computed execute'

        state.name 所在的 effect 函數執行時,birth_year.value的屬性訪問器也會被觸發,收集依賴并執行計算屬性的effect函數。

        修改計算屬性的value訪問器,根據前面聲明的公共屬性this._dirty,判斷當前_dirty(臟值)是否為 true 來決定是否收集依賴和重新獲取新值:

        get value() {
            if (this._dirty) {
                this._value = this.effect()
                track(this, 'value')
                this._dirty = false
            }
            return this._value
        }

        scheduler函數被執行時,說明值被修改,需要重新設置_dirty:

        constructor(getter, public setter) {
            this.effect = effect(getter, {
                lazy: true,
                scheduler: () => {
        +            this._dirty = true
                    trigger(this, TriggerType.set, 'value')
                },
            })
        }

        完整的 computed.ts:

        import { isFunction } from '../shared/index'
        import { effect, track, trigger, TriggerType } from './effect'
        
        class ComputedRefImpl {
          public effect
          public __v_isReadonly = true
          public __v_isRef = true // 判斷是否直接返回 value 值
          public _dirty = true // 緩存數據
          private _value
          constructor(getter, public setter) {
            this.effect = effect(getter, {
              lazy: true, // lazy=true 默認不會執行
              scheduler: () => {
                this._dirty = true
                trigger(this, TriggerType.set, 'value')
              },
            })
          }
          get value() {
            if (this._dirty) {
              this._value = this.effect()
              track(this, 'value')
              this._dirty = false
            }
            return this._value
          }
          set value(newValue) {
            this.setter(newValue)
          }
        }
        
        export function computed(getterOrOptions) {
          let getter
          let setter
        
          if (isFunction(getterOrOptions)) {
            getter = getterOrOptions
            setter = () => console.log('computed not set value')
          } else {
            getter = getterOrOptions.get
            setter = getterOrOptions.set
          }
        
          return new ComputedRefImpl(getter, setter)
        }

        Ref

        ref的實現,判斷傳入值是不是對象,對象用reactive包裹處理,獲取值時收集依賴,設置值時觸發更新

        import { hasChanged, isObject } from "../shared/index";
        import { track, trigger, TriggerType } from "./effect";
        import { reactive } from "./reactive";
        
        const convert = (val) => isObject(val) ? reactive(val) : val;
        
        class RefImpl {
            public readonly __v_isRef = true;
            private _value;
            constructor(private _rawValue){
                this._value = convert(_rawValue)
            }
            get value(){
                track(this,"value")
                return this._value
            }
            set value(newValue){
                if (hasChanged(newValue, this._rawValue)) {
                    this._rawValue = newValue;
                    this._value = convert(newValue);
                    trigger(this, TriggerType.set, "value");
                }
            }
        }
        
        export function ref(rawValue){
            return new RefImpl(rawValue)
        }
        查看原文

        贊 0 收藏 0 評論 0

        chenwl 贊了文章 · 3月10日

        巧用 TypeScript(五)---- infer

        介紹

        infer 最早出現在此 PR 中,表示在 extends 條件語句中待推斷的類型變量。

        簡單示例如下:

        type ParamType<T> = T extends (param: infer P) => any ? P : T;

        在這個條件語句 T extends (param: infer P) => any ? P : T 中,infer P 表示待推斷的函數參數。

        整句表示為:如果 T 能賦值給 (param: infer P) => any,則結果是 (param: infer P) => any 類型中的參數 P,否則返回為 T。

        interface User {
          name: string;
          age: number;
        }
        
        type Func = (user: User) => void
        
        type Param = ParamType<Func>;   // Param = User
        type AA = ParamType<string>;    // string

        內置類型

        在 2.8 版本中,TypeScript 內置了一些與 infer 有關的映射類型:

        • 用于提取函數類型的返回值類型:

          type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

          相比于文章開始給出的示例,ReturnType<T> 只是將 infer P 從參數位置移動到返回值位置,因此此時 P 即是表示待推斷的返回值類型。

          type Func = () => User;
          type Test = ReturnType<Func>;   // Test = User
        • 用于提取構造函數中參數(實例)類型:

          一個構造函數可以使用 new 來實例化,因此它的類型通常表示如下:

          type Constructor = new (...args: any[]) => any;

          infer 用于構造函數類型中,可用于參數位置 new (...args: infer P) => any; 和返回值位置 new (...args: any[]) => infer P;。

          因此就內置如下兩個映射類型:

          // 獲取參數類型
          type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
          
          // 獲取實例類型
          type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
          
          class TestClass {
          
            constructor(
              public name: string,
              public string: number
            ) {}
          }
          
          type Params = ConstructorParameters<typeof TestClass>;  // [string, numbder]
          
          type Instance = InstanceType<typeof TestClass>;         // TestClass

        一些用例

        至此,相信你已經對 infer 已有基本了解,我們來看看一些使用它的「騷操作」:

        • tupleunion ,如:[string, number] -> string | number

          解答之前,我們需要了解 tuple 類型在一定條件下,是可以賦值給數組類型:

          type TTuple = [string, number];
          type TArray = Array<string | number>;
          
          type Res = TTuple extends TArray ? true : false;    // true
          type ResO = TArray extends TTuple ? true : false;   // false

          因此,在配合 infer 時,這很容做到:

          type ElementOf<T> = T extends Array<infer E> ? E : never
          
          type TTuple = [string, number];
          
          type ToUnion = ElementOf<ATuple>; // string | number

          stackoverflow 上看到另一種解法,比較簡(牛)單(逼):

          type TTuple = [string, number];
          type Res = TTuple[number];  // string | number
        • unionintersection,如:string | number -> string & number

          這個可能要稍微麻煩一點,需要 infer 配合「 Distributive conditional types 」使用。

          相關鏈接中,我們可以了解到「Distributive conditional types」是由「naked type parameter」構成的條件類型。而「naked type parameter」表示沒有被 Wrapped 的類型(如:Array<T>、[T]、Promise<T> 等都是不是「naked type parameter」)?!窪istributive conditional types」主要用于拆分 extends 左邊部分的聯合類型,舉個例子:在條件類型 T extends U ? X : Y 中,當 TA | B 時,會拆分成 A extends U ? X : Y | B extends U ? X : Y;

          有了這個前提,再利用在逆變位置上,同一類型變量的多個候選類型將會被推斷為交叉類型的特性,即

          type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
          type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
          type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

          因此,綜合以上幾點,我們可以得到在 stackoverflow 上的一個答案:

          type UnionToIntersection<U> =
            (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
          
          type Result = UnionToIntersection<string | number>; // string & number

          當傳入 string | number 時:

          • 第一步:(U extends any ? (k: U) => void : never) 會把 union 拆分成 (string extends any ? (k: string) => void : never) | (number extends any ? (k: number)=> void : never),即是得到 (k: string) => void | (k: number) => void;
          • 第二步:(k: string) => void | (k: number) => void extends ((k: infer I)) => void ? I : never,根據上文,可以推斷出 Istring & number。

        當然,你可以玩出更多花樣,比如 uniontuple。

        LeetCode 的一道 TypeScript 面試題

        前段時間,在 GitHub 上,發現一道來自 LeetCode TypeScript 的面試題,比較有意思,題目的大致意思是:

        假設有一個這樣的類型(原題中給出的是類,這里簡化為 interface):

        interface Module {
          count: number;
          message: string;
          asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
          syncMethod<T, U>(action: Action<T>): Action<U>;
        }

        在經過 Connect 函數之后,返回值類型為

        type Result {
          asyncMethod<T, U>(input: T): Action<U>;
          syncMethod<T, U>(action: T): Action<U>;
        }

        其中 Action<T> 的定義為:

        interface Action<T> {
          payload?: T
          type: string
        }

        這里主要考察兩點

        • 挑選出函數
        • 此篇文章所提及的 infer

        挑選函數的方法,已經在 handbook 中已經給出,只需判斷 value 能賦值給 Function 就行了:

        type FuncName<T>  = {
          [P in keyof T]: T[P] extends Function ? P : never;
        }[keyof T];
        
        type Connect = (module: Module) => { [T in FuncName<Module>]: Module[T] }
        /*
         * type Connect = (module: Module) => {
         *   asyncMethod: <T, U>(input: Promise<T>) => Promise<Action<U>>;
         *   syncMethod: <T, U>(action: Action<T>) => Action<U>;
         * }
        */

        接下來就比較簡單了,主要是利用條件類型 + infer,如果函數可以賦值給 asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>,則取值為 asyncMethod<T, U>(input: T): Action<U>。具體答案就不給出了,感興趣的小伙伴可以嘗試一下。

        更多

        參考

        更多文章,請關注我們的公眾號:

        微信服務號

        查看原文

        贊 40 收藏 24 評論 4

        chenwl 贊了文章 · 2月26日

        nodejs 終端打印進度條

        1. 場景導入

        當我們對大量文件進行批量處理的時候(例如:上傳/下載、保存、編譯等),常常希望知道當前進展如何,或者失敗(成功)的任務有多少;當我們的代碼或程序已經發布,用戶在執行安裝的過程中,一個合適的(終端/命令行)進度條可以準確反映安裝的步驟和進程,提升程序的可用性,一定程度緩解用戶在等待中的煩惱……

        2. 基本原理

        首先,在終端打印出文本是件比較容易的事情。
        那么使用簡單的文本和符號,就夠自己拼湊出命令行的效果(下面例子):

        文件已上傳: 43.60% █████████████████████????????????????????????????? 150/344

        當然,進度條的效果可以根據需要自己設計啦,我這里只是給大家一個參考。

        這里,我將打印命令行的方法構造成一個工具模塊 progress-bar.js,具體實現如下 :-)

        // 這里用到一個很實用的 npm 模塊,用以在同一行打印文本
        var slog = require('single-line-log').stdout;
        
        // 封裝的 ProgressBar 工具
        function ProgressBar(description, bar_length){
          // 兩個基本參數(屬性)
          this.description = description || 'Progress';       // 命令行開頭的文字信息
          this.length = bar_length || 25;                     // 進度條的長度(單位:字符),默認設為 25
        
          // 刷新進度條圖案、文字的方法
          this.render = function (opts){
            var percent = (opts.completed / opts.total).toFixed(4);    // 計算進度(子任務的 完成數 除以 總數)
            var cell_num = Math.floor(percent * this.length);             // 計算需要多少個 █ 符號來拼湊圖案
        
            // 拼接黑色條
            var cell = '';
            for (var i=0;i<cell_num;i++) {
              cell += '█';
            }
        
            // 拼接灰色條
            var empty = '';
            for (var i=0;i<this.length-cell_num;i++) {
              empty += '?';
            }
        
            // 拼接最終文本
            var cmdText = this.description + ': ' + (100*percent).toFixed(2) + '% ' + cell + empty + ' ' + opts.completed + '/' + opts.total;
            
            // 在單行輸出文本
            slog(cmdText);
          };
        }
        
        // 模塊導出
        module.exports = ProgressBar;

        3. Run 起來

        基于上面的實現,先說一下這個 progress-bar.js 的用法:

        // 引入工具模塊
        var ProgressBar = require('./progress_bar');
        
        // 初始化一個進度條長度為 50 的 ProgressBar 實例
        var pb = new ProgressBar('下載進度', 50);
        
        // 這里只是一個 pb 的使用示例,不包含任何功能
        var num = 0, total = 200;
        function downloading() {
          if (num <= total) {
            // 更新進度條
            pb.render({ completed: num, total: total });
        
            num++;
            setTimeout(function (){
              downloading();
            }, 500)
          }
        }
        downloading();

        run 一下上面的代碼,執行效果如下:

        原創文章,轉載請注明出處

        查看原文

        贊 6 收藏 5 評論 1

        chenwl 發布了文章 · 2月20日

        defer-promise搞定異步彈窗組件

        最近在看vue組件庫 Plain UI 時,發現一個比較有趣的異步彈框組件寫法,操作如下:

        <div class="dialog">
            <input type="text" name="message">
            <button type="button">cancel</button>
            <button type="button">confirm</button>
        </div>
        (async ()=>{
            let message = await openDialog();
            console.log("彈窗信息",message)
        })()

        在異步函數中打開彈窗 openDialog 方法,當用戶點擊 confirm 按鈕后,彈窗關閉,返回輸入框信息。

        openDialog方法可以很方便的通過promise實現,不過在看組件庫源碼時,發現對方是用defer 實現的,在promise兼容性還不是很好的時代 JQuery 就已經有 deferred.promise() 方法了,這里順便也做了溫習。

        defer方法:

        const defer = () => {
            const def = {}
            def.promise = new Promise((resolve, reject) => {
              def.resolve = resolve
              def.reject = reject
            })
            return def
        }

        defer方法其實返回的也是一個promise,并且將 resolvereject 方法拆開,這樣我們就可以選擇在適當的時機調用 resolve 或者 reject 方法了。

        const dialogController = () => {
          let dfd = null
        
          const confirmBtn = document.getElementById('confirm')
          // 點擊確定按鈕
          confirmBtn.addEventListener('click', () => {
          // 隱藏彈窗
            dialogEl.hide()
          // resolve輸入框信息給用戶
            dfd.resolve(inputEl.value)
          })
        
          return () => {
            dfd = defer()
            dialogEl.show()
            return dfd.promise
          }
        }

        獲得打開彈窗promise方法:

        const openDialog = dialogController()

        控制彈窗的打開,在異步函數中如果用戶點擊了彈窗確定按鈕,關閉彈窗,獲得輸入信息。

        const controlBtn = document.getElementById('control')
        controlBtn.addEventListener('click', async () => {
          const message = await openDialog()
          console.log("彈窗輸入框信息:",message)
        })

        這種方式可以方便我們封裝常用的業務組件,之前在看 axios.cancel 源碼時里面也是使用這種套路,靈活且實用。

        通過 defer 方式實現的彈窗代碼:

        <html>
          <head>
            <title>defer promise</title>
            <style>
              .dialog {
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                display: flex;
                position: fixed;
                align-items: center;
                pointer-events: none;
                justify-content: center;
              }
              .dialog .mask {
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                position: absolute;
                opacity: 0;
                transition: 0.3s;
                background-color: rgba(0, 0, 0, 0.4);
              }
        
              .dialog-content {
                padding: 20px;
                transition: 0.2s;
                opacity: 0;
                transform: scale(0.95);
                background-color: #fff;
              }
        
              .dialog.visible {
                pointer-events: all;
              }
              .dialog.visible .mask {
                opacity: 1;
              }
              .dialog.visible .dialog-content {
                opacity: 1;
                transform: scale(1);
              }
            </style>
          </head>
          <body>
            <div class="container">
              <button id="control">顯示彈窗</button>
              <div class="dialog" id="dialog">
                <div class="mask" onclick="this.parentNode.classList.remove('visible')"></div>
                <div class="dialog-content">
                  <input type="text" id="content" />
                  <button id="confirm">確定</button>
                </div>
              </div>
            </div>
        
            <script>
              const defer = () => {
                const def = {}
                def.promise = new Promise((resolve, reject) => {
                  def.resolve = resolve
                  def.reject = reject
                })
        
                return def
              }
        
              ;(() => {
                const inputEl = document.getElementById('content')
                const dialogEl = document.getElementById('dialog')
                dialogEl.show = () => dialogEl.classList.add('visible')
                dialogEl.hide = () => dialogEl.classList.remove('visible')
        
                const dialogController = () => {
                  let dfd = null
                  const confirmBtn = document.getElementById('confirm')
                  confirmBtn.addEventListener('click', () => {
                    dialogEl.hide()
                    dfd.resolve(inputEl.value)
                  })
        
                  return () => {
                    dfd = defer()
                    dialogEl.show()
                    return dfd.promise
                  }
                }
        
                const openDialog = dialogController()
                const controlBtn = document.getElementById('control')
                controlBtn.addEventListener('click', async () => {
                  const message = await openDialog()
                  console.log('彈窗輸入框信息:', message)
                })
              })()
            </script>
          </body>
        </html>
        
        查看原文

        贊 0 收藏 0 評論 0

        chenwl 贊了文章 · 2月4日

        前端mock完美解決方案實戰

        寫在前面,本文閱讀需要一定Nodejs的相關知識,因為會擴展webpack的相關功能,并且實現需要遵守一定約定和Ajax封裝。沉淀的腳手架也放到Github上供給同學參考React-Starter, 使用手冊還沒寫完善, 整體思路和React還是Vue無關,如果對大家有收獲記得Star下。
        它有這些功能:

        • 開發打包有不同配置
        • eslint 驗證
        • 代碼風格統一
        • commit 規范驗證
        • 接口mock
        • 熱更新
        • 異步組件

        Mock功能介紹

        市面上講前端mock怎么做的文章很多,整體上閱讀下來的沒有一個真正站在前端角度上讓我覺得強大和易用的。下面就說下我期望的前端mock要有哪些功能:

        1. mock功能和前端代碼解耦
        2. 一個接口支持多種mock情況
        3. 無需依賴另外的后端服務和第三方庫
        4. 能在network看到mock接口的請求且能區分
        5. mock數據、接口配置和頁面在同一個目錄下
        6. mock配置改變無需重啟前端dev
        7. 生產打包可以把mock數據注入到打包的js中走前端mock
        8. 對于后端已有的接口也能快速把Response數據轉化為mock數據

        上面的這些功能我講其中幾點的作用:

        對于第7點的作用是后續項目開發完成,在完全沒有開發后端服務的情況下,也可以進行演示。這對于一些ToB定制的項目來沉淀項目地圖(案例)很有作用。
        對于第8點在開發環境后端服務經常不穩定下,不依賴后端也能做頁面開發,核心是能實現一鍵生成mock數據。

        配置解耦

        耦合情況

        什么是前端配置解耦,首先讓我們看下平時配置耦合情況有哪些:

        • webpack-dev后端測試環境變了需要改git跟蹤的代碼
        • dev和build的時候 需要改git跟蹤的代碼
        • 開發的時候想這個接口mock 需要改git跟蹤的代碼 mockUrl ,mock?

        如何解決

        前端依賴的配置解耦的思路是配置文件conf.json是在dev或build的時候動態生成的,然后該文件在前端項目引用:

        ├── config
        │   ├── conf.json                                    # git 不跟蹤
        │   ├── config.js                                    # git 不跟蹤
        │   ├── config_default.js
        │   ├── index.js
        │   └── webpack.config.js
        ├── jsconfig.json
        ├── mock.json                                            # git 不跟蹤

        webpack配置文件引入js的配置,生成conf.json

        // config/index.js
        const _ = require("lodash");
        let config = _.cloneDeep(require("./config_default"))
        try {
          const envConfig = require('./config') // eslint-disable-line
          config = _.merge(config, envConfig);
        } catch (e) {
            // 
        }
        module.exports = config;

        默認使用config_default.js 的內容,如果有config.js 則覆蓋,開發的時候復制config_default.js 為config.js 后續相關配置可以修改config.js即可。

        // config/config_default.js
        const pkg = require("../package.json");
        module.exports = {
          projectName: pkg.name,
          version: pkg.version,
          port: 8888,
          proxy: {
            "/render-server/api/*": {
              target: `http://192.168.1.8:8888`,
              changeOrigin: true, // 支持跨域請求
              secure: true, // 支持 https
            },
          },
          ...
          conf: {
            dev: {
              title: "前端模板",
              pathPrefix: "/react-starter", // 統一前端路徑前綴
              apiPrefix: "/api/react-starter", //
              debug: true,
              delay: 500,    // mock數據模擬延遲
              mock: {
                // "global.login": "success",
                // "global.loginInfo": "success",
              }
            },
            build: {
              title: "前端模板",
              pathPrefix: "/react-starter",
              apiPrefix: "/api/react-starter",
              debug: false,
              mock: {}
            }
          }
        };
        

        在開發或打包的時候根據環境變量使用conf.dev或conf.build 生成conf.json文件內容

        // package.json
        {
          "name": "react-starter",
          "version": "1.0.0",
          "description": "react前端開發腳手架",
          "main": "index.js",
          "scripts": {
            "start": "webpack-dev-server --config './config/webpack.config.js' --open --mode development",
            "build": "cross-env BUILD_ENV=VERSION webpack --config './config/webpack.config.js' --mode production --progress --display-modules && npm run tar",
            "build-mock": "node ./scripts/build-mock.js "
          },
          ...
        }

        指定webpack路徑是./config/webpack.config.js

        然后在webpack.config.js中引入配置并生成conf.json文件

        // config/webpack.config.js
        const config = require('.')
        const env = process.env.BUILD_ENV ? 'build' : 'dev'
        const confJson = env === 'build' ? config.conf.build : config.conf.dev
        fs.writeFileSync(path.join(__dirname, './conf.json'),  JSON.stringify(confGlobal, null, '\t'))

        引用配置

        src/common/utils.jsx文件中暴露出配置項,配置也可以通過window.conf來覆蓋

        // src/common/utils.jsx
        import conf from '@/config/conf.json'
        export const config = Object.assign(conf, window.conf)

        然后就可以在各個頁面中使用

        import {config} from '@src/common/utils'
        class App extends Component {
          render() {
            return (
              <Router history={history}>
                <Switch>
                  <Route path={`${config.pathPrefix}`} component={Home} />
                  <Redirect from="/" to={`${config.pathPrefix}`} />
                </Switch>
              </Router>
            )
          }
        }
        ReactDOM.render(
             <App />,
          document.getElementById('root'),
        )

        Mock實現

        效果

        為了實現我們想要的mock的相關功能,首先是否開啟mock的配置解耦可以通過上面說的方式來實現,我們一般在頁面異步請求的時候都會目錄定義一個io.js的文件, 里面定義了當前頁面需要調用的相關后端接口:

        // src/pages/login/login-io.js
        import {createIo} from '@src/io'
        
        const apis = {
          // 登錄
          login: {
            method: 'POST',
            url: '/auth/login',
          },
          // 登出
          logout: {
            method: 'POST',
            url: '/auth/logout',
          },
        }
        export default createIo(apis, 'login') // 對應login-mock.json

        上面定義了登錄和登出接口,我們希望對應開啟的mock請求能使用當前目錄下的login-mock.json文件的內容

        // src/pages/login/login-mock.json
        {
            "login": {
                "failed": {
                    "success": false,
                    "code": "ERROR_PASS_ERROR",
                    "content": null,
                    "message": "賬號或密碼錯誤!"
                },
                "success": {
                    "success": true,
                    "code": 0,
                    "content": {
                        "name": "admin",
                        "nickname": "超級管理員",
                        "permission": 15
                    },
                    "message": ""
                }
            },
            "logout": {
                "success": {
                    "success": true,
                    "code": 0,
                    "content": null,
                    "message": ""
                }
            }
        }

        在調用logout登出這個Ajax請求的時候且我們的conf.json中配置的是"login.logout": "success" 就返回login-mock.json中的login.success 的內容,配置沒有匹配到就請求轉發到后端服務。

        // config/conf.json
        {
            "title": "前端后臺模板",
            "pathPrefix": "/react-starter",
            "apiPrefix": "/api/react-starter",
            "debug": true,
            "delay": 500,
            "mock": {
                "login.logout": "success"
            }
        }

        這是我們最終要實現的效果,這里有一個約定:項目目錄下所有以-mock.jsom文件結尾的文件為mock文件,且文件名不能重復。

        思路

        在webpack配置項中devServer的proxy配置接口的轉發設置,接口轉發使用了功能強大的 http-proxy-middleware 軟件包, 我們約定proxy的配置格式是:

          proxy: {
            "/api/react-starter/*": {
              target: `http://192.168.90.68:8888`,
              changeOrigin: true,
              secure: true,
              // onError: (),
              // onProxyRes,
              // onProxyReq  
            },
          },

        它有幾個事件觸發的配置:

        • option.onError 出現錯誤
        • option.onProxyRes 后端響應后
        • option.onProxyReq 請求轉發前
        • option.onProxyReqWs
        • option.onOpen
        • option.onClose

        所以我們需要定制這幾個事情的處理,主要是請求轉發前和請求處理后

        onProxyReq

        想在這里來實現mock的處理, 如果匹配到了mock數據我們就直接響應,就不轉發請求到后端。 怎么做呢: 思路是依賴請求頭,dev情況下前端在調用的時候能否注入約定好的請求頭 告訴我需要尋找哪個mock數據項, 我們約定Header:

        • mock-key 來匹配mock文件如login-mock.json的內容, 如login
        • mock-method 來匹配對應文件內容的方法項 如logout

        然后conf.json中mock配置尋找到具體的響應項目如:"login.logout": "success/failed"的內容

        onProxyRes

        如果調用了真實的后端請求,就把請求的響應數據緩存下來,緩存到api-cache目錄下文件格式mock-key.mock-method.json

        ├── api-cache                                    # git 不跟蹤
        │   ├── login.login.json
        │   └── login.logout.json
        // api-cache/global.logout.json
        {
            "success": {
                "date": "2020-11-17 05:32:17",
                "method": "POST",
                "path": "/render-server/api/logout",
                "url": "/render-server/api/logout",
                "resHeader": {
                    "content-type": "application/json; charset=utf-8",
              ...
                },
                "reqHeader": {
                    "host": "127.0.0.1:8888",
                    "mock-key": "login",
                    "mock-method": "logout"
              ...
                },
                "query": {},
                "reqBody": {},
                "resBody": {
                    "success": true,
                    "code": 0,
                    "content": null,
                    "message": ""
                }
            }
        }

        這樣做的目的是為了后續實現一鍵生成mock文件。

        前端接口封裝

        使用

        上面我們看到定義了接口的io配置:

        // src/pages/login/login-io.js
        import {createIo} from '@src/io'
        
        const apis = {
          // 登錄
          login: {
            method: 'POST',
            url: '/auth/login',
          },
          // 登出
          logout: {
            method: 'POST',
            url: '/auth/logout',
          },
        }
        export default createIo(apis, 'login') // login注冊到header的mock-key

        我們在store中使用

        // src/pages/login/login-store.js
        
        import {observable, action, runInAction} from 'mobx'
        import io from './login-io'
        // import {config, log} from './utils'
        
        export class LoginStore {
          // 用戶信息
          @observable userInfo
          // 登陸操作
          @action.bound
          async login(params) {
            const {success, content} = await io.login(params)
            if (!success) return
            runInAction(() => {
              this.userInfo = content
            })
          }
        }
        export default LoginStore

        通過 createIo(apis, 'login') 的封裝在調用的時候就可以非常簡單的來傳遞請求參數,簡單模式下會判斷參數是到body還是到query中。 復雜的也可以支持比如可以header,query, body等這里不演示了。

        createIo 請求封裝

        這個是前端接口封裝的關鍵地方,也是mock請求頭注入的地方

        // src/io/index.jsx
        import {message, Modal} from 'antd'
        import {config, log, history} from '@src/common/utils'
        import {ERROR_CODE} from '@src/common/constant'
        import creatRequest from '@src/common/request'
        let mockData = {}
        try {
          // eslint-disable-next-line global-require, import/no-unresolved
          mockData = require('@/mock.json')
        } catch (e) {
          log(e)
        }
        
        let reloginFlag = false
        // 創建一個request
        export const request = creatRequest({
          // 自定義的請求頭
          headers: {'Content-Type': 'application/json'},
          // 配置默認返回數據處理
          action: (data) => {
            // 統一處理未登錄的彈框
            if (data.success === false && data.code === ERROR_CODE.UN_LOGIN && !reloginFlag) {
              reloginFlag = true
              // TODO 這里可能統一跳轉到 也可以是彈窗點擊跳轉
              Modal.confirm({
                title: '重新登錄',
                content: '',
                onOk: () => {
                  // location.reload()
                  history.push(`${config.pathPrefix}/login?redirect=${window.location.pathname}${window.location.search}`)
                  reloginFlag = false
                },
              })
            }
          },
          // 是否錯誤顯示message
          showError: true,
          message,
          // 是否以拋出異常的方式 默認false {success: boolean判斷}
          throwError: false,
          // mock 數據請求的等待時間
          delay: config.delay,
          // 日志打印
          log,
        })
        
        // 標識是否是簡單傳參數, 值為true標識復雜封裝
        export const rejectToData = Symbol('flag')
        
        /**
         * 創建請求IO的封裝
         * @param ioContent {any { url: string method?: string mock?: any apiPrefix?: string}}
          }
         * @param name mock數據的對應文件去除-mock.json后的
         */
        export const createIo = (ioContent, name = '') => {
          const content = {}
          Object.keys(ioContent).forEach((key) => {
            /**
             * @param {baseURL?: string, rejectToData?: boolean,  params?: {}, query?: {}, timeout?: number, action?(data: any): any, headers?: {},  body?: any, data?: any,   mock?: any}
             * @returns {message, content, code,success: boolean}
             */
            content[key] = async (options = {}) => {
              // 這里判斷簡單請求封裝 rejectToData=true 表示復雜封裝
              if (!options[rejectToData]) {
                options = {
                  data: options,
                }
              }
              delete options[rejectToData]
              if (
                config.debug === false &&
                name &&
                config.mock &&
                config.mock[`${name}.${key}`] &&
                mockData[name] &&
                mockData[name][key]
              ) { // 判斷是否是生產打包 mock注入到代碼中
                ioContent[key].mock = JSON.parse(JSON.stringify(mockData[name][key][config.mock[`${name}.${key}`]]))
              } else if (name && config.debug === true) { //注入 mock請求頭
                if (options.headers) {
                  options.headers['mock-key'] = name
                  options.headers['mock-method'] = key
                } else {
                  options.headers = {'mock-key': name, 'mock-method': key}
                }
              }
              const option = {...ioContent[key], ...options}
        
              option.url = ((option.apiPrefix ? option.apiPrefix : config.apiPrefix) || '') + option.url
        
              return request(option)
            }
          })
          return content
        }

        這里對request也做進一步的封裝,配置項設置了一些默認的處理設置。比如通用的請求響應失敗的是否有一個message, 未登錄的情況是否有一個彈窗提示點擊跳轉登陸頁。如果你想定義多個通用處理可以再創建一個request2和createIo2。

        request封裝axios

        是基于axios的二次封裝, 并不是非常通用,主要是在約定的請求失敗和成功的處理有定制,如果需要可以自己修改使用。

        import axios from 'axios'
        
        // 配置接口參數
        // declare interface Options {
        //   url: string
        //   baseURL?: string
        //   // 默認GET
        //   method?: Method
        //   // 標識是否注入到data參數
        //   rejectToData?: boolean
        //   // 是否直接彈出message 默認是
        //   showError?: boolean
        //   // 指定 回調操作 默認登錄處理
        //   action?(data: any): any
        //   headers?: {
        //     [index: string]: string
        //   }
        //   timeout?: number
        //   // 指定路由參數
        //   params?: {
        //     [index: string]: string
        //   }
        //   // 指定url參數
        //   query?: any
        //   // 指定body 參數
        //   body?: any
        //   // 混合處理 Get到url, delete post 到body, 也替換路由參數 在createIo封裝
        //   data?: any
        //   mock?: any
        // }
        // ajax 請求的統一封裝
        // TODO 1. 對jsonp請求的封裝 2. 重復請求
        
        /**
         * 返回ajax 請求的統一封裝
         * @param Object option 請求配置
         * @param {boolean} opts.showError 是否錯誤調用message的error方法
         * @param {object} opts.message  包含 .error方法 showError true的時候調用
         * @param {boolean} opts.throwError 是否出錯拋出異常
         * @param {function} opts.action  包含 自定義默認處理 比如未登錄的處理
         * @param {object} opts.headers  請求頭默認content-type: application/json
         * @param {number} opts.timeout  超時 默認60秒
         * @param {number} opts.delay   mock請求延遲
         * @returns {function} {params, url, headers, query, data, mock} data混合處理 Get到url, delete post 到body, 也替換路由參數 在createIo封裝
         */
        export default function request(option = {}) {
          return async (optionData) => {
            const options = {
              url: '',
              method: 'GET',
              showError: option.showError !== false,
              timeout: option.timeout || 60 * 1000,
              action: option.action,
              ...optionData,
              headers: {'X-Requested-With': 'XMLHttpRequest', ...option.headers, ...optionData.headers},
            }
            // 簡單請求處理
            if (options.data) {
              if (typeof options.data === 'object') {
                Object.keys(options.data).forEach((key) => {
                  if (key[0] === ':' && options.data) {
                    options.url = options.url.replace(key, encodeURIComponent(options.data[key]))
                    delete options.data[key]
                  }
                })
              }
              if ((options.method || '').toLowerCase() === 'get' || (options.method || '').toLowerCase() === 'head') {
                options.query = Object.assign(options.data, options.query)
              } else {
                options.body = Object.assign(options.data, options.body)
              }
            }
            // 路由參數處理
            if (typeof options.params === 'object') {
              Object.keys(options.params).forEach((key) => {
                if (key[0] === ':' && options.params) {
                  options.url = options.url.replace(key, encodeURIComponent(options.params[key]))
                }
              })
            }
            // query 參數處理
            if (options.query) {
              const paramsArray = []
              Object.keys(options.query).forEach((key) => {
                if (options.query[key] !== undefined) {
                  paramsArray.push(`${key}=${encodeURIComponent(options.query[key])}`)
                }
              })
              if (paramsArray.length > 0 && options.url.search(/\?/) === -1) {
                options.url += `?${paramsArray.join('&')}`
              } else if (paramsArray.length > 0) {
                options.url += `&${paramsArray.join('&')}`
              }
            }
            if (option.log) {
              option.log('request  options', options.method, options.url)
              option.log(options)
            }
            if (options.headers['Content-Type'] === 'application/json' && options.body && typeof options.body !== 'string') {
              options.body = JSON.stringify(options.body)
            }
            let retData = {success: false}
            // mock 處理
            if (options.mock) {
              retData = await new Promise((resolve) =>
                setTimeout(() => {
                  resolve(options.mock)
                }, option.delay || 500),
              )
            } else {
              try {
                const opts = {
                  url: options.url,
                  baseURL: options.baseURL,
                  params: options.params,
                  method: options.method,
                  headers: options.headers,
                  data: options.body,
                  timeout: options.timeout,
                }
                const {data} = await axios(opts)
                retData = data
              } catch (err) {
                retData.success = false
                retData.message = err.message
                if (err.response) {
                  retData.status = err.response.status
                  retData.content = err.response.data
                  retData.message = `瀏覽器請求非正常返回: 狀態碼 ${retData.status}`
                }
              }
            }
        
            // 自動處理錯誤消息
            if (options.showError && retData.success === false && retData.message && option.message) {
              option.message.error(retData.message)
            }
            // 處理Action
            if (options.action) {
              options.action(retData)
            }
            if (option.log && options.mock) {
              option.log('request response:', JSON.stringify(retData))
            }
            if (option.throwError && !retData.success) {
              const err = new Error(retData.message)
              err.code = retData.code
              err.content = retData.content
              err.status = retData.status
              throw err
            }
            return retData
          }
        }
        一鍵生成mock

        根據api-cache下的接口緩存和定義的xxx-mock.json文件來生成。

        # "build-mock": "node ./scripts/build-mock.js"
        # 所有:
        npm run build-mock mockAll 
        # 單個mock文件:
        npm run build-mock login
        # 單個mock接口:
        npm run build-mock login.logout
        # 復雜 
        npm run build-mock login.logout user

        具體代碼參考build-mock.js

        mock.json文件生成

        為了在build打包的時候把mock數據注入到前端代碼中去,使得mock.json文件內容盡可能的小,會根據conf.json的配置項來動態生成mock.json的內容,如果build里面沒有開啟mock項,內容就會是一個空json數據。 當然后端接口代理處理內存中也映射了一份該mock.json的內容。這里需要做幾個事情:

        • 根據配置動態生成mock.json的內容
        • 監聽config文件夾 判斷關于mock的配置項是否有改變重新生成mock.json
        // scripts/webpack-init.js 在wenpack配置文件中初始化
        const path = require('path')
        const fs = require('fs')
        const {syncWalkDir} = require('./util')
        let confGlobal = {}
        let mockJsonData = {}
        exports.getConf = () => confGlobal
        exports.getMockJson =() => mockJsonData
        
        /**
         * 初始化項目的配置 動態生成mock.json和config/conf.json
         * @param {string} env  dev|build
         */
         exports.init = (env = process.env.BUILD_ENV ? 'build' : 'dev') => {
           
          delete require.cache[require.resolve('../config')]
          const config  = require('../config')
          const confJson = env === 'build' ? config.conf.build : config.conf.dev
          confGlobal = confJson
          // 1.根據環境變量來生成
          fs.writeFileSync(path.join(__dirname, '../config/conf.json'),  JSON.stringify(confGlobal, null, '\t'))
          buildMock(confJson)
         }
         
         // 生成mock文件數據
         const buildMock = (conf) => {
          // 2.動態生成mock數據 讀取src文件夾下面所有以 -mock.json結尾的文件 存儲到io/index.json文件當中
          let mockJson = {}
          const mockFiles = syncWalkDir(path.join(__dirname, '../src'), (file) => /-mock.json$/.test(file))
          console.log('build mocks: ->>>>>>>>>>>>>>>>>>>>>>>')
          mockFiles.forEach((filePath) => {
            const p = path.parse(filePath)
            const mockKey = p.name.substr(0, p.name.length - 5)
            console.log(mockKey, filePath)
            if (mockJson[mockKey]) {
              console.error(`有相同的mock文件名稱${p.name} 存在`, filePath)
            }
            delete require.cache[require.resolve(filePath)]
            mockJson[mockKey] = require(filePath)
          })
          // 如果是打包環境, 最小化mock資源數據
          const mockMap = conf.mock || {}
          const buildMockJson = {}
          Object.keys(mockMap).forEach((key) => {
            const [name, method] = key.split('.')
            if (mockJson[name][method] && mockJson[name][method][mockMap[key]]) {
              if (!buildMockJson[name]) buildMockJson[name] = {}
              if (!buildMockJson[name][method]) buildMockJson[name][method] = {}
              buildMockJson[name][method][mockMap[key]] = mockJson[name][method][mockMap[key]]
            }
          })
          mockJsonData = buildMockJson
          fs.writeFileSync(path.join(__dirname, '../mock.json'), JSON.stringify(buildMockJson, null, '\t'))
         }
         
         // 監聽配置文件目錄下的config.js和config_default.js
        const confPath = path.join(__dirname, '../config')
        
        if ((env = process.env.BUILD_ENV ? 'build' : 'dev') === 'dev') {
          fs.watch(confPath, async (event, filename) => {
            if (filename === 'config.js' || filename === 'config_default.js') {
              delete require.cache[path.join(confPath, filename)]
              delete require.cache[require.resolve('../config')]
              const config  = require('../config')
              // console.log('config', JSON.stringify(config))
              const env = process.env.BUILD_ENV ? 'build' : 'dev'
              const confJson = env === 'build' ? config.conf.build : config.conf.dev
              if (JSON.stringify(confJson) !== JSON.stringify(confGlobal)) {
                this.init()
              }
            }
          });
        }

        接口代理處理

        onProxyReq和onProxyRes

        實現上面思路里面說的onProxyReq和onProxyRes 響應處理

        util.js

        // scripts/api-proxy-cache 
        const fs = require('fs')
        const path = require('path')
        const moment = require('moment')
        const {getConf, getMockJson} = require('./webpack-init')
        const API_CACHE_DIR = path.join(__dirname, '../api-cache')
        const {jsonParse, getBody} = require('./util')
        
        fs.mkdirSync(API_CACHE_DIR,{recursive: true})
        
        module.exports = {
          // 代理前處理
          onProxyReq: async (_, req, res) => {
            req.reqBody = await getBody(req)
            const {'mock-method': mockMethod, 'mock-key': mockKey} = req.headers
            // eslint-disable-next-line no-console
            console.log(`Ajax 請求: ${mockKey}.${mockMethod}`,req.method, req.url)
            // eslint-disable-next-line no-console
            req.reqBody && console.log(JSON.stringify(req.reqBody, null, '\t'))
            if (mockKey && mockMethod) {
              req.mockKey = mockKey
              req.mockMethod = mockMethod
              const conf = getConf()
              const mockJson = getMockJson()
              if (conf.mock && conf.mock[`${mockKey}.${mockMethod}`] && mockJson[mockKey] && mockJson[mockKey][mockMethod]) {
                // eslint-disable-next-line no-console
                console.log(`use mock data ${mockKey}.${mockMethod}:`, conf.mock[`${mockKey}.${mockMethod}`], 'color: green')
                res.mock = true
                res.append('isMock','yes')
                res.send(mockJson[mockKey][mockMethod][conf.mock[`${mockKey}.${mockMethod}`]])
              }
             
            }
          },
          // 響應緩存接口
          onProxyRes: async (res, req) => {
            const {method, url, query, path: reqPath, mockKey, mockMethod} = req
            
            if (mockKey && mockMethod && res.statusCode === 200) {
              
              let resBody = await getBody(res)
              resBody = jsonParse(resBody)
              const filePath = path.join(API_CACHE_DIR, `${mockKey}.${mockMethod}.json`)
              let  data = {}
              if (fs.existsSync(filePath)) {
                data = jsonParse(fs.readFileSync(filePath).toString())
              }
              const cacheObj = {
                date: moment().format('YYYY-MM-DD hh:mm:ss'),
                method,
                path: reqPath,
                url,
                resHeader: res.headers,
                reqHeader: req.headers,
                query,
                reqBody: await jsonParse(req.reqBody),
                resBody: resBody
              }
              if (resBody.success === false) {
                data.failed = cacheObj
              } else {
                data.success = cacheObj
              }
              // eslint-disable-next-line no-console
              fs.writeFile(filePath, JSON.stringify(data,'', '\t'), (err) => { err && console.log('writeFile', err)})
            }
          },
          // 后端服務沒啟的異常處理
          onError(err, req, res) {
            setTimeout(() => {
             if (!res.mock) {
               res.writeHead(500, {
                 'Content-Type': 'text/plain',
               });
               res.end('Something went wrong. And we are reporting a custom error message.');
             }
           }, 10)
          }
        }
        webpack配置

        在webpack配置中引入使用

        const config = require('.')
        // config/webpack.config.js
        const {init} = require('../scripts/webpack-init');
        init();
        // 接口請求本地緩存
        const apiProxyCache = require('../scripts/api-proxy-cache')
        for(let key in config.proxy) {
          config.proxy[key] = Object.assign(config.proxy[key], apiProxyCache);
        }
        
        const webpackConf = {
          devServer: {
            contentBase: path.join(__dirname, '..'), // 本地服務器所加載的頁面所在的目錄
            inline: true,
            port: config.port,
            publicPath: '/',
            historyApiFallback: {
              disableDotRule: true,
              // 指明哪些路徑映射到哪個html
              rewrites: config.rewrites,
            },
            host: '127.0.0.1',
            hot: true,
            proxy: config.proxy,
          },
        }
        

        總結

        mock做好其實在我們前端實際中還是很有必要的,做過的項目如果后端被鏟除了想要回憶就可以使用mock讓項目跑起來,可以尋找一些實現的效果來進行代碼復用。當前介紹的mock流程實現有很多定制的開發,但是真正完成后,團隊中的成員只是使用還是比較簡單配置即可。

        關于前端項目部署我也分享了一個BFF 層,當前做的還不是很完善,也分享給大家參考

        Render-Server 主要功能包含:

        • 一鍵部署 npm run deploy
        • 支持集群部署配置
        • 是一個文件服務
        • 是一個靜態資源服務
        • 在線可視化部署前端項目
        • 配置熱更新
        • 在線Postman及接口文檔
        • 支持前端路由渲染, 支持模板
        • 接口代理及路徑替換
        • Web安全支持 Ajax請求驗證,Referer 校驗
        • 支持插件開發和在線配置 可實現: 前端模板參數注入、請求頭注入、IP白名單、接口mock、會話、第三方登陸等等
        查看原文

        贊 21 收藏 15 評論 6

        chenwl 發布了文章 · 1月29日

        axios取消功能詳解

        axios提供 CancelToken 方法可以取消正在發送中的接口請求。

        官方提供了兩種方式取消發送,第一種方式如下:

        const CancelToken = axios.CancelToken;
        const source = CancelToken.source();
        
        axios.get('/user/12345', {
          cancelToken: source.token
        }).catch(function (thrown) {
          if (axios.isCancel(thrown)) {
            console.log('Request canceled', thrown.message);
          } else {
            // handle error
          }
        });
        
        axios.post('/user/12345', {
          name: 'new name'
        }, {
          cancelToken: source.token
        })
        
        // cancel the request (the message parameter is optional)
        source.cancel('Operation canceled by the user.');

        第二種方式如下:

        const CancelToken = axios.CancelToken;
        let cancel;
        
        axios.get('/user/12345', {
          cancelToken: new CancelToken(function executor(c) {
            // An executor function receives a cancel function as a parameter
            cancel = c;
          })
        });
        
        // cancel the request
        cancel();

        官方實現取消功能的文件存放在 /lib/cancel/CancelToken.js

        雖然代碼不多,但是第一次看真是一頭霧水,下面就來抽絲剝繭,一步步還原里面的實現邏輯。

        分析

        兩種方式都調用了 CancekToken 這個構造函數,我們就先從這個構造函數開始。

        分析:

        第一種方式:

        • CancekToken 提供一個靜態方法source,source方法返回tokencancel方法

        第二種方式:

        • CancekToken 接收一個回調函數作為參數,回調函數接收cancel取消方法

        第二種方式更容易入手,我們可以先實現構造函數CancekToken,再考慮第一種方式靜態方法source的實現。

        簡易版 axios

        首先我們寫個簡易版的axios,方便我們后面的分析和調試:

        知識點:Promise、XMLHttpRequest

        function axios(url,config){
          return new Promise((resolve,reject)=>{
            const xhr = new XMLHttpRequest();
            xhr.open(config.method || "GET",url);
            xhr.responseType = config.responseType || "json";
            xhr.onload = ()=>{
              if(xhr.readyState === 4 && xhr.status === 200){
                resolve(xhr.response);
              }else{
                reject(xhr)
              }
            };
            xhr.send(config.data ? JSON.stringify(config.data) : null);
          })
        }

        CancelToken

        第二種方式中,我們可以看到 CancelToken 在配置參數cancelToken中實例化:

        axios.get('/user/12345', {
          cancelToken: new CancelToken
        });

        所以在axios中,我們也會根據配置中是否包含cancelToken來取消發送:

        function axios(url,config){
          return new Promise((resolve,reject)=>{
            const xhr = new XMLHttpRequest();
            ...
            if(config.cancelToken){
              // 如果存在 cancelToken 參數
              // xhr.abort() 終止發送任務
              // reject() 走reject方法
            }
            ...

        回到配置參數,CancelToken接受一個回調函數作為參數,參數包含取消的cancel方法,我們初始化CancelToken方法如下:

        function CancelToken(executor){
          let cancel = ()=>{};
          executor(cancel)
        }

        回到官方例子,例子中參數cancel方法被賦值給當前環境的cancel變量,于是當前環境cancel變量指向CancelToken方法中的cancel函數表達式。

        let cancel;
        axios.get('/user/12345', {
          cancelToken: new CancelToken(function executor(c) {
            cancel = c; // 指向CancelToken中的 cancel 方法
          })
        });

        接下來cancel方法一旦被執行,就能觸發請求終止(發布訂閱)。

        這里官方源碼巧妙的使用了Promise鏈式調用的方式實現,我們給CancelToken方法返回一個Promise方法:

        function CancekToken(executor){
          let cancel = ()=>{};
          const promise = new Promise(resolve => cancel = resolve);
          executor(cancel);
          return promise;
        }

        接下來只要用戶執行cancel方法,配置參數cancelToken獲得的Promise方法就能響應了:

        let cancel;
        axios.get('/user/12345', {
          cancelToken: new CancelToken(function executor(c) {
            cancel = c; // 指向CancelToken中的 cancel 方法
          })
        });
        // 執行
        + cancel("canceled request");
        這里可以把 cancel 理解成 Promise.resolve

        axios中響應cancel方法:

        function axios(url,config){
          return new Promise((resolve,reject)=>{
            const xhr = new XMLHttpRequest();
            ...
            if(config.cancelToken){
        +       config.cancelToken.then(reason=>{
        +        xhr.abort();
        +        reject(reason);
              })
            }
            ...

        關鍵點是把 Promise.resolve 從函數內部抽出來,巧妙的實現了異步分離

        到了這里,第二種方法的取消功能就基本實現了。

        CancekToken.source

        source作為 CancekToken 提供的靜態方法,返回tokencancel 方法。

        cancel方法跟前面的功能是一樣的,可以理解成局部環境里面聲明好cancel再拋出來。

        我們再來看看第二種方式 token 在配置中的使用:

        const CancelToken = axios.CancelToken;
        const source = CancelToken.source();
        
        axios.get('/user/12345', {
          cancelToken: source.token // token 返回的是CancelToken實例
        })

        根據前面的配置我們可以知道 source.token 實際上返回的是 CancelToken 實例。

        了解 source 方法需要返回的對象功能后,就可以輕松實現source方法了:

        CancekToken.source = function(){
        
          let cancel = ()=>{};
          const token = new CancekToken(c=>cancel = c);
        
          return {
            token,
            cancel
          }
        }

        axios.isCancel

        通過上的代碼我們知道,取消請求會走reject方法,在Promise中可以被catch到,不過我們還需要判斷catch的錯誤是否來自取消方法,這里官方提供了isCancel方法判斷:

        axios.get('/user/12345', {
          cancelToken: source.token
        }).catch(function (error) {
          // 判斷是否 取消操作
          if (axios.isCancel(error)) {}
        });

        在js中我們可以通過instanceof判斷是否來自某個構造函數的實例,這里新建Cancel方法來管理取消發送的信息:

        function Cancel(reason){
          this.message = reason;
        }

        CancekToken.source 返回的cancel方法通過函數包裝,實例化一個Cancel作為取消參數:

        CancekToken.source = function(){
          
        -  let cancel = ()=>{};
        -  const token = new CancekToken(c=>cancel = c);
          
        + let resolve = ()=>{};
        +  let token = new CancekToken(c=>resolve = c);
        
          return {
            token,
        -    cancel,
        +   cancel:(reason)=>{
        +     // 實例化一個小 cancel,將 reason 傳入
        +     resolve(new Cancel(reason))
        +   }
          }
        }

        最終Promise.catch到的參數來自實例Cancel,就可以很容易的判斷error是否來自Cancel了:

        function isCancel(error){
          return error instanceof Cancel
        }
        // 將 `isCancel` 綁定到 axios
        axios.isCancel = isCancel

        最后,官方還判斷了CancelToken.prototype.throwIfRequested,如果調用了cancel方法,具有相同cancelToken配置的ajax請求也不會被發送,這里可以參考官方代碼的實現。

        全部代碼

        最后是全部代碼實現:

        function Cancel(reason) {
          this.message = reason
        }
        
        function CancekToken(executor) {
          let reason = null
          let resolve = null
          const cancel = message => {
            if(reason) return;
            reason = new Cancel(message);
            resolve(reason)
          }
          const promise = new Promise(r => (resolve = r))
          executor(cancel)
          return promise
        }
        
        CancekToken.source = function() {
          let cancel = () => {}
          let token = new CancekToken(c => (cancel = c))
        
          return {
            token,
            cancel
          }
        }
        
        const source = CancekToken.source()
        
        axios('/simple/get', {
          cancelToken: source.token
        }).catch(error => {
          if (axios.isCancel(error)) {
            console.log(error)
          }
        })
        
        source.cancel('canceled http request 1')
        
        let cancel
        axios('/simple/get', {
          cancelToken: new CancekToken(c => {
            cancel = c
          })
        }).catch(error => {
          if (axios.isCancel(error)) {
            console.log(error)
          }
        })
        cancel('canceled http request 2')
        
        function axios(url, config) {
          return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest()
            xhr.open(config.method || 'GET', url)
            xhr.responseType = config.responseType || 'json'
        
            if (config.cancelToken) {
              config.cancelToken.then(reason => {
                xhr.abort()
                reject(reason)
              })
            }
        
            xhr.onload = () => {
              if (xhr.readyState === 4 && xhr.status === 200) {
                resolve(xhr.response)
              } else {
                reject(xhr)
              }
            }
            xhr.send(config.data ? JSON.stringify(config.data) : null)
          })
        }
        
        axios.isCancel = function(error) {
          return error instanceof Cancel
        }

        es6簡易版本的實現:

        export class Cancel {
            public reason:string;
            constructor(reason:string){
                this.reason = reason
            }
        }
        export function isCancel(error:any){
            return error instanceof Cancel;
        }
        export class CancelToken {
            public resolve:any;
            source(){
                return {
                    token:new Promise(resolve=>{
                        this.resolve = resolve;
                    }),
                    cancel:(reason:string)=>{
                        this.resolve(new Cancel(reason).reason)
                    }
                }
            }
        }
        查看原文

        贊 1 收藏 0 評論 0

        chenwl 贊了文章 · 1月28日

        網易云音樂年度歌單的卡通形象聯動制作

        最近朋友圈被很多網易云音樂的年底歌單給刷屏了, 我也去看了我的年度歌單, 發現一個有意思的交互效果, 選擇卡通形象, 通過滑動選擇人物的不同頭像,衣服,褲子 最終塑造成一個擁有獨立個性的卡通形象.

        界面效果預覽

        image

        交互效果預覽

        image

        image

        制作素材

        把每個滑動的圖片進行了全屏截圖, 然后通過圖片處理工具去除背景, 制作成統一大小的png圖片.

        image
        image
        image

        圖片的卡通元素都是通過截圖獲取, 每個元素被處理成統一大小, 部分會有鋸齒, 僅供參考. 這里頭部比較特殊, 每個形象的頭部大小不一, 這里取一個統一的截止線, 方便后面整合成整個形象. 其它類似,頂對齊即可.

        分析交互的特點

        1. 輪播圖
        2. 跨屏
        3. 滑動循環
        4. 部分衣服滑動會觸發褲子的改變
        5. 部分褲子滑動會觸發衣服的改變
        6. ...
        

        image
        輪播圖代碼

        <div id="slide" class="bui-slide bui-slide-skin01"></div>
        var uiSlide = bui.slide({
                id: "#slide",
                height: 320,
                // autopage: true, // 自動分頁
                data: [{
                    image: "images/banner01.png",
                    url: "pages/ui_controls/bui.slide_title.html",
                }, {
                    image: "images/banner02.png",
                    url: "pages/ui_controls/bui.slide_title.html",
                }, {
                    image: "images/banner03.png",
                    url: "pages/ui_controls/bui.slide_title.html",
                }],
                loop: true, // 循環
            })

        image

        跨屏輪播圖只需加上 cross:true 參數即可. 熟悉BUI的朋友, 一眼就能找到類似的效果, 跨屏輪播圖 第1-第3的特點就解決了.

        有意思的是第4點第5點, 輪播圖切換的時候部分需要相互關聯.

        實現的核心思路:

        1. 頁面有一個靜態全屏輪播圖, 用于點擊下一步,上一步的整屏切換. 靜態輪播圖的好處是結構可以自定義.
        2. 首屏初始化三個跨屏輪播圖, 用于頭部,衣服,褲子的正常選擇切換;
        3. 點擊輪播圖的時候, 切換激活狀態, 非激活狀態隱藏左右兩個圖片(隱藏通過css), 并禁止滑動 ;
        4. 當滑動選中以后,分別把頭部,衣服,褲子的圖片地址,索引 緩存在 bui.store (輪播圖的to回調里面);
        5. 通過bui.store 創建衣服跟褲子的關聯 conection 字段, 當檢測到滑動的圖片有配套褲子的時候,自動滑動下一個輪播圖到指定位置;
        6. 點擊下一步去到第2屏, 用于展示剛剛選中的數據;
        // 衣服
        const cartoonBody = bui.slide({
            id: "#cartoonBody",
            height: 320,
            stopPropagation: false,
            autopage: false,
            cross: true,
            loop: true,
            data: this.$data.cartoon.body
        }).on("to", function () {
            let index = this.index();
            // bui.store 讀取的時候需要使用 this.$data.xxx ,如果使用 this.xxx 讀取會導致最終的值不能設置正確.
            let img = that.$data.cartoon.body[index].image;
            // 設置
            that.profile.body.image = img;
            that.profile.body.index = index;
        
            // 檢測衣服跟褲子的關系索引
            let item = bui.array.get(that.$data.conection, img, "body");
            let footindex = bui.array.index(that.$data.cartoon.foot, item.foot, "image");
        
            if (footindex >= 0 && that.$data.active[1] == "active-block") {
                // 操作褲子的實例, 跳轉的時候, 由于loop:true, 這里的索引需要在真實的索引下+1 
                that.$data.distances[2].to(footindex + 1, "none")
            }
        
        }).lock();// lock禁止滑動
        // 褲子
        const cartoonFoot = bui.slide({
            id: "#cartoonFoot",
            height: 320,
            stopPropagation: false,
            autopage: false,
            cross: true,
            loop: true,
            data: this.$data.cartoon.foot
        }).on("to", function () {
            let index = this.index();
            let img = that.$data.cartoon.foot[index].image;
            that.profile.foot.image = img;
            that.profile.foot.index = index;
        
            // 檢測衣服跟褲子的關系索引
            let item = bui.array.get(that.$data.conection, img, "foot");
            let bodyindex = bui.array.index(that.$data.cartoon.body, item.body, "image");
            if (bodyindex >= 0 && that.$data.active[2] == "active-block") {
                // 操作衣服的實例, 跳轉的時候, 由于loop:true, 這里的索引需要在真實的索引下+1 
                that.$data.distances[1].to(bodyindex + 1, "none")
            }
        }).lock();// lock禁止滑動

        最終效果

        image
        image

        github地址: https://github.com/imouou/BUI...

        codepen地址: https://codepen.io/imouou/ful...

        BUI專注移動開發, 靈活超出你的想象, 感謝您的閱讀.

        image.png

        多頁完整代碼

        <!DOCTYPE HTML>
        <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
            <title>BUI</title>
            <meta name="format-detection" content="telephone=no" />
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
            <link rel="stylesheet"  />
            
        <style>
            .cartoon-page main,
            .step-item {
                background-color: #f2c9bc;
                padding-top: .2rem;
            }
            .step-item {
                width: 100%;
                height: 100%;
            }
            .cartoon-page h1,
            .cartoon-page p {
                text-align: center;
                color: #675553;
            }
            .cartoon-wrap .bui-slide {
                margin-bottom: .2rem;
            }
            .cartoon-wrap .bui-slide-img{
                width: 4rem;
                height: 3.2rem;
                background-color: #e2b4a3;
                border-radius: .2rem;
            }
            .cartoon-wrap .active-block .bui-slide-img{
                background-color: #fff;
            }
            .cartoon-wrap .active-block .bui-cross-prev,
            .cartoon-wrap .active-block .bui-cross-next{
                visibility: visible;
            }
            .cartoon-wrap  .bui-cross-prev,
            .cartoon-wrap  .bui-cross-next{
                visibility: hidden;
            }
            .cartoon-wrap  .bui-cross-prev .bui-slide-img,
            .cartoon-wrap  .bui-cross-next .bui-slide-img{
                background-color: rgba(255,255,255,.3);
            }
            .bui-btn-step {
                width: 1.4rem;
                height: 1.4rem;
                line-height: 1.4rem;
                color: #fff;
                background-color: #f5433b;
                border: 3px solid rgba(255,255,255,0.8);
                padding: 0;
                margin-bottom: .2rem;
            }
            .bui-slide-cross .bui-cross-next .bui-slide-img, 
            .bui-slide-cross .li-next .bui-slide-img{
                margin-left: 0;
            }
            .bui-slide-cross .bui-cross-prev .bui-slide-img, 
            .bui-slide-cross .li-prev .bui-slide-img{
                margin-right: 0;
            }
            .bui-slide-fullscreen>.bui-slide-main>ul>li img.cartoonhead ,
            .bui-slide-fullscreen>.bui-slide-main>ul>li img.cartoonbody,
            .bui-slide-fullscreen>.bui-slide-main>ul>li img.cartoonfoot {
                display: block;
                width:3.2rem ;
                height:3.2rem ;
            }
            .cartoonhead {
                position: relative;
                z-index: 3;
            }
            .cartoonbody {
                margin-top: -1.1rem;
                position: relative;
                z-index: 2;
            }
            .cartoonfoot {
                margin-top: -1.1rem;
                position: relative;
                z-index: 1;
            }
        </style>
        </head>
        <body>
        <!-- HTML Begin-->
        
        <!-- 這里還是一個標準的BUI頁面 -->
        <div class="bui-page bui-box-vertical cartoon-page">
            <header></header>
            <main>
                <!-- 靜態輪播圖 -->
                <div id="uiSlide" class="bui-slide">
                    <div class="bui-slide-main">
                        <ul>
                            <li>
                                <!-- 垂直布局 -->
                                <div class="step-item bui-box-center bui-box-vertical fullheight">
        
                                    <div class="span1">
                                        <h1>設置形象, 開啟年度報告</h1>
                                        <p>左右切換選擇造型</p>
                                        <div class="bui-box bui-box-vertical cartoon-wrap">
                                            <div class="span1" b-class="cartoons.active.0" b-click="cartoons.activeBlock(0)">
                                                <div id="cartoonHead" class="bui-slide"></div>
                                            </div>
                                            <div class="span1" b-class="cartoons.active.1" b-click="cartoons.activeBlock(1)">
                                                <div id="cartoonBody" class="bui-slide"></div>
                                            </div>
                                            <div class="span1" b-class="cartoons.active.2" b-click="cartoons.activeBlock(2)">
                                                <div id="cartoonFoot" class="bui-slide"></div>
                                            </div>
                                            <!-- <div class="span1" b-class="cartoons.active.3" b-click="cartoons.activeBlock(3)">
                                                <div id="cartoonDeco" class="bui-slide"></div>
                                            </div> -->
                                        </div>
                                    </div>
                                    <div class="container-y">
                                        <div class="bui-btn-step ring" b-click="cartoons.next">下一步</div>
                                    </div>
                                </div>
                            </li>
                            <li style="display:none;">
                                <!-- 垂直布局 -->
                                <div class="step-item bui-box-center bui-box-vertical fullheight">
                                    <!-- 最終形象 -->
                                    <div class="span1">
                                        <div class="bui-box-center">
                                            <div class="wrap-img">
                                                ![](cartoons.profile.head.image)
                                                ![](cartoons.profile.body.image)
                                                ![](cartoons.profile.foot.image)
                                            </div>
                                        </div>
                                    </div>
                                    <div class="container-y">
                                        <div class="bui-btn-step ring" b-click="cartoons.prev">上一步</div>
                                    </div>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </main>
        </div>
        <!-- HTML End-->
            <!-- 依賴庫 手機調試的js引用順序如下 -->
            <script data-original="https://cdn.jsdelivr.net/npm/buijs@latest/lib/zepto.js"></script>
            <script data-original="https://cdn.jsdelivr.net/npm/buijs@latest/lib/latest/bui.js"></script>
            <script>
                bui.ready(function () {
                    // 這里寫業務及控件初始化, 一個頁面只能有一個bui.ready
                    // 頁面跳轉的全屏輪播圖
                    const uiSlideStep = bui.slide({
                        id: "#uiSlide",
                        autopage: false,
                        fullscreen: true,
                        swipe: false,
                        loop: false
                    })
                    // 初始化數據行為存儲
                    const bs = bui.store({
                        el: `.bui-page`,
                        scope: "cartoons",
                        data: {
                            // 衣服褲子的關系, 部分衣服關聯褲子, 褲子關聯衣服
                            conection: [{
                                body: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body02.png",
                                foot: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot01.png"
                            }, {
                                body: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body03.png",
                                foot: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot05.png"
                            }, {
                                body: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body12.png",
                                foot: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot08.png"
                            }, {
                                body: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body13.png",
                                foot: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot07.png"
                            }, {
                                body: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body14.png",
                                foot: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot06.png"
                            }],
                            distances: [], // 存儲滑動的實例
                            active: {
                                0: "active-block",
                                1: "",
                                2: "",
                            },
                            profile: {
                                // 個人形象的存儲
                                head: {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head01.png",
                                    index: 0,
                                },
                                body: {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body01.png",
                                    index: 0,
                                },
                                foot: {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot01.png",
                                    index: 0,
                                },
                                deco: {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco01.png",
                                    index: 0,
                                }
                            },
                            cartoon: {
                                active: 0, // 激活的slide, 默認頭部
                                head: [{
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head01.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head02.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head03.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head04.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head05.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head06.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head07.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head08.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head09.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head10.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head11.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/head/head12.png",
                                }],
                                body: [{
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body01.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body02.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body03.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body04.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body05.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body06.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body07.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body08.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body09.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body10.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body11.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body12.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body13.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/body/body14.png",
                                }],
                                foot: [{
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot01.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot02.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot03.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot04.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot05.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot06.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot07.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot08.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/foot/foot09.png",
                                }],
                                deco: [{
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco01.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco02.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco03.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco04.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco05.png",
                                }, {
                                    image: "https://gitee.com/imouou/bui-case-cartoon/raw/main/src/images/cartoon/deco/deco06.png",
                                }],
                            },
                        },
                        methods: {
                            activeBlock(index) {
                                for (let i = 0; i < Object.keys(this.$data.active).length; i++) {
                                    this.active[i] = "";
                                    this.$data.distances[i].lock();
                                }
                                // 給激活的滑動圖加上樣式,區別其它兩個
                                this.active[index] = "active-block";
                                this.$data.distances[index].unlock();
                            },
                            next() {
                                uiSlideStep.next();
                            },
                            prev() {
                                uiSlideStep.prev();
                            }
                        },
                        mounted: function () {
                            // 焦點圖 js 初始化:
                            let that = this;
                            const cartoonHead = bui.slide({
                                id: "#cartoonHead",
                                height: 320,
                                autopage: false,
                                stopPropagation: false,
                                cross: true,
                                loop: true,
                                data: this.$data.cartoon.head
                            }).on("to", function () {
                                let index = this.index();
                                // bui.store 讀取的時候需要使用 this.$data.xxx ,如果使用 this.xxx 讀取會導致最終的值不能設置正確.
                                let img = that.$data.cartoon.head[index].image;
                                // 設置
                                that.profile.head.index = index;
                                that.profile.head.image = img;
        
                            })
        
                            const cartoonBody = bui.slide({
                                id: "#cartoonBody",
                                height: 320,
                                stopPropagation: false,
                                autopage: false,
                                cross: true,
                                loop: true,
                                data: this.$data.cartoon.body
                            }).on("to", function () {
                                let index = this.index();
                                // bui.store 讀取的時候需要使用 this.$data.xxx ,如果使用 this.xxx 讀取會導致最終的值不能設置正確.
                                let img = that.$data.cartoon.body[index].image;
                                // 設置
                                that.profile.body.image = img;
                                that.profile.body.index = index;
        
                                // 檢測衣服跟褲子的關系索引
                                let item = bui.array.get(that.$data.conection, img, "body");
                                let footindex = bui.array.index(that.$data.cartoon.foot, item.foot, "image");
        
                                if (footindex >= 0 && that.$data.active[1] == "active-block") {
                                    // 操作褲子的實例, 跳轉的時候, 由于loop:true, 這里的索引需要在真實的索引下+1 
                                    that.$data.distances[2].to(footindex + 1, "none")
                                }
        
                            }).lock();
        
                            const cartoonFoot = bui.slide({
                                id: "#cartoonFoot",
                                height: 320,
                                stopPropagation: false,
                                autopage: false,
                                cross: true,
                                loop: true,
                                data: this.$data.cartoon.foot
                            }).on("to", function () {
                                let index = this.index();
                                let img = that.$data.cartoon.foot[index].image;
                                that.profile.foot.image = img;
                                that.profile.foot.index = index;
        
                                // 檢測衣服跟褲子的關系索引
                                let item = bui.array.get(that.$data.conection, img, "foot");
                                let bodyindex = bui.array.index(that.$data.cartoon.body, item.body, "image");
                                if (bodyindex >= 0 && that.$data.active[2] == "active-block") {
                                    // 操作衣服的實例, 跳轉的時候, 由于loop:true, 這里的索引需要在真實的索引下+1 
                                    that.$data.distances[1].to(bodyindex + 1, "none")
                                }
                            }).lock();
        
                            // const cartoonDeco = bui.slide({
                            //     id: "#cartoonDeco",
                            //     height: 320,
                            //     stopPropagation: false,
                            //     autopage: false,
                            //     cross: true,
                            //     loop: true,
                            //     data: this.$data.cartoon.deco
                            // }).on("to", function () {
                            //     let index = this.index();
        
                            //     that.profile.deco.image = that.$data.cartoon.deco[index].image
                            //     that.profile.deco.index = index;
                            // }).to(0, "none").lock();
        
                            // 添加實例,跟cartoon.active 的數值對應.
                            this.distances.push(cartoonHead, cartoonBody, cartoonFoot);
        
                        }
                    })
                })
            </script>
        </body>
        </html>
        查看原文

        贊 1 收藏 0 評論 0

        chenwl 發布了文章 · 1月5日

        dva-loading使用總結

        在開發異步加載的功能時,為提高用戶體驗一般會顯示加載提示,最近在使用umi做項目時接觸到dva-loading,對全局和局部組件的異步加載控制還是非常方便的。

        在umi中使用

        安裝和配置

        安裝:

        $ npm install dva-loading -S

        進入 src/app.js 進行 運行時dva配置

        import createLoading from "dva-loading"
        
        export const dva = {
          plugins: [createLoading()]
        }

        models

        models 文件夾下新建 count.js,輸入下面內容:

        const delay = (ms)=>new Promise(r=>setTimeout(r,ms))
        
        export default {
            namespace:"count",
            state:{
                count:1,
            },
            effects:{
                *add(action,{put,call}){
                    yield call(delay,1000);
                    yield put({type:"change",payload:Math.random()})
                }
            },
            reducers:{
                change(state,{payload}){
                    return {count:state.count+payload}
                }
            }
        }

        組件中使用

        新建 Count.js組件進行測試:

        import React from "react"
        import { connect } from "dva"
        
        function Count({ dispatch, count, loading }) {
            
            const isLoading = loading.models.count;
            // 單獨對 effects 控制
            // const isLoading = loading.effects["count/add"]
            // 對多個 effects 控制
            // const isLoading = loading.effects["count/add"] || loading.effects["count/minus"] || false;
        
          return (
            <div>
              {isLoading ? <p>加載中...</p> : <p>{count}</p>}
              <button onClick={() => dispatch({ type: "count/add" })}>+</button>
            </div>
          )
        }
        
        export default connect((state) => ({ ...state.count, loading: state.loading }))(Count)

        我們可以通過 state.loading 判斷組件的 model甚至 effect 的狀態。

        dva-loading 源碼

        dva-loading

        const SHOW = '@@DVA_LOADING/SHOW';
        const HIDE = '@@DVA_LOADING/HIDE';
        const NAMESPACE = 'loading';
        
        function createLoading(opts = {}) {
          const namespace = opts.namespace || NAMESPACE;
        
          const { only = [], except = [] } = opts;
          if (only.length > 0 && except.length > 0) {
            throw Error('It is ambiguous to configurate `only` and `except` items at the same time.');
          }
        
          const initialState = {
            global: false,
            models: {},
            effects: {},
          };
        
          const extraReducers = {
            [namespace](state = initialState, { type, payload }) {
              const { namespace, actionType } = payload || {};
              let ret;
              switch (type) {
                case SHOW:
                  ret = {
                    ...state,
                    global: true,
                    models: { ...state.models, [namespace]: true },
                    effects: { ...state.effects, [actionType]: true },
                  };
                  break;
                case HIDE: {
                  const effects = { ...state.effects, [actionType]: false };
                  const models = {
                    ...state.models,
                    [namespace]: Object.keys(effects).some(actionType => {
                      const _namespace = actionType.split('/')[0];
                      if (_namespace !== namespace) return false;
                      return effects[actionType];
                    }),
                  };
                  const global = Object.keys(models).some(namespace => {
                    return models[namespace];
                  });
                  ret = {
                    ...state,
                    global,
                    models,
                    effects,
                  };
                  break;
                }
                default:
                  ret = state;
                  break;
              }
              return ret;
            },
          };
        
          function onEffect(effect, { put }, model, actionType) {
            const { namespace } = model;
            if (
              (only.length === 0 && except.length === 0) ||
              (only.length > 0 && only.indexOf(actionType) !== -1) ||
              (except.length > 0 && except.indexOf(actionType) === -1)
            ) {
              return function*(...args) {
                yield put({ type: SHOW, payload: { namespace, actionType } });
                yield effect(...args);
                yield put({ type: HIDE, payload: { namespace, actionType } });
              };
            } else {
              return effect;
            }
          }
        
          return {
            extraReducers,
            onEffect,
          };
        }
        
        export default createLoading;

        @umijs/plugin-dva 接口實現

        @umijs/plugin-dva 拋出的 useSelector 方法可以很方便的幫助我們獲取models 層數據:

        const { loading, count } = useSelector((stores) => ({ 
              loading: stores.loading, 
              count: stores.count 
            }))

        通過 useDispatch 獲取 dispatch 方法:

        const dispatch = useDispatch()
        const add = () => dispatch({ type: "count/add" })

        修改狀態:

        import React from "react"
        import { useDispatch, useSelector } from "dva"
        
        function Count(props) {
          const dispatch = useDispatch()
          const add = () => dispatch({ type: "count/add" })
        
          const { loading, count } = useSelector((stores) => ({ 
              loading: stores.loading, 
              count: stores.count 
            }))
          const isLoading = loading.models.count
        
          return (
            <div>
              {isLoading ? <p>loading</p> : <p>{count.count}</p>}
              <button onClick={add}>+</button>
            </div>
          )
        }
        
        export default Count

        全局 loading 控制

        通過 useSelector方法得到 stores.loading.global,判斷 models是否在loading中:

        import React from 'react'
        const {useSelector} = 'dva'
        import {Spin} from 'antd'
        const DemoPage = () => {
          const {loading} = useSelector(stores => ({
            loading: stores.loading
          }))
          return (
            <Spin spinning={loading.global}/>
          )
        }

        參考:

        查看原文

        贊 0 收藏 0 評論 0

        chenwl 發布了文章 · 2020-12-24

        nodejs篇-進程與集群cluster

        我們啟動一個服務、運行一個實例,就是開一個服務進程,Node.js 里通過 node app.js 開啟一個服務進程,多進程就是進程的復制(fork),fork 出來的每個進程都擁有自己的獨立空間地址、數據棧,一個進程無法訪問另外一個進程里定義的變量、數據結構,只有建立了 IPC 通信,進程之間才可數據共享。

        child_process

        node.js中可以通過下面四種方式創建子進程:

        • child_process.spawn(command, args)
        • child_process.exec(command, options)
        • child_process.execFile(file, args[, callback])
        • child_process.fork(modulePath, args)

        spawn

        const {spawn} = require("child_process");
        // 創建 文件
        spawn("touch",["index.js"]);

        spawn()會返回child-process子進程實例:

        const {spawn} = require("child_process");
        // cwd 指定子進程的工作目錄,默認當前目錄
        const child = spawn("ls",["-l"],{cwd:__dirname});
        // 輸出進程信息
        child.stdout.pipe(process.stdout);
        console.log(process.pid,child.pid);

        子進程同樣基于事件機制(EventEmitter API),提供了一些事件:

        • exit:子進程退出時觸發,可以得知進程退出狀態(code和signal)
        • disconnect:父進程調用child.disconnect()時觸發
        • error:子進程創建失敗,或被kill時觸發
        • close:子進程的stdio流(標準輸入輸出流)關閉時觸發
        • message:子進程通過process.send()發送消息時觸發,父子進程消息通信
        close與exit的區別主要體現在多進程共享同一stdio流的場景,某個進程退出了并不意味著stdio流被關閉了

        子進程具有可讀流的特性,利用可讀流實現find . -type f | wc -l,遞歸統計當前目錄文件數量:

        const { spawn } = require('child_process');
        
        const find = spawn('find', ['.', '-type', 'f']);
        const wc = spawn('wc', ['-l']);
        
        find.stdout.pipe(wc.stdin);
        
        wc.stdout.on('data', (data) => {
          console.log(`Number of files ${data}`);
        });

        exec

        spawn()exec()方法的區別在于,exec()不是基于stream的,exec()會將傳入命令的執行結果暫存到buffer中,再整個傳遞給回調函數。

        spawn()默認不會創建shell去執行命令(性能上會稍好),而exec()方法執行是會先創建shell,所以可以在exec()方法中傳入任意shell腳本。

        const {exec} = require("child_process");
        
        exec("node -v",(error,stdout,stderr)=>{
            if (error) console.log(error);
            console.log(stdout)
        })
        exec()方法因為可以傳入任意shell腳本所以存在安全風險。

        spawn()方法默認不會創建shell去執行傳入的命令(所以性能上稍微好一點),不過可以通過參數實現:

        const { spawn } = require('child_process');
        const child = spawn('node -v', {
          shell: true
        });
        child.stdout.pipe(process.stdout);

        這種做法的好處是,既能支持shell語法,也能通過stream IO進行標準輸入輸出。

        execFile

        const {execFile} = require("child_process");
        
        execFile("node",["-v"],(error,stdout,stderr)=>{
            console.log({ error, stdout, stderr })
            console.log(stdout)
        })

        通過可執行文件路徑執行:

        const {execFile} = require("child_process");
        
        execFile("/Users/.nvm/versions/node/v12.1.0/bin/node",["-v"],(error,stdout,stderr)=>{
            console.log({ error, stdout, stderr })
            console.log(stdout)
        })

        fork

        fork()方法可以用來創建Node進程,并且父子進程可以互相通信

        //master.js
        const {fork} = require("child_process");
        const worker = fork("worker.js");
        
        worker.on("message",(msg)=>{
            console.log(`from worder:${msg}`)
        });
        worker.send("this is master");
        
        // worker.js
        process.on("message",(msg)=>{
            console.log("worker",msg)
        });
        process.send("this is worker");

        利用fork()可以用來處理計算量大,耗時長的任務:

        const longComputation = () => {
          let sum = 0;
          for (let i = 0; i < 1e10; i++) {
            sum += i;
          };
          return sum;
        };

        longComputation方法拆分到子進程中,這樣主進程的事件循環不會被耗時計算阻塞:

        const http = require('http');
        const { fork } = require('child_process');
        
        const server = http.createServer();
        
        server.on('request', (req, res) => {
          if (req.url === '/compute') {
            // 將計算量大的任務,拆分到子進程中處理
            const compute = fork('compute.js');
            compute.send('start');
            compute.on('message', sum => {
                // 收到子進程任務后,返回
              res.end(`Sum is ${sum}`);
            });
          } else {
            res.end('Ok')
          }
        });
        
        server.listen(3000);

        進程間通信IPC

        每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,InterProcess Communication)

        進程之間可以借助內置的IPC機制通信

        父進程:

        • 接收事件process.on('message')
        • 發送信息給子進程master.send()

        子進程:

        • 接收事件process.on('message')
        • 發送信息給父進程process.send()

        fork 多進程

        nodejs中的多進程是 多進程 + 單線程 的模式
        // master.js. 
        process.title = 'node-master'
        const net = require("net");
        const {fork} = require("child_process");
        
        const handle = net._createServerHandle("127.0.0.1",3000);
        
        for(let i=0;i<4;i++){
            fork("./worker.js").send({},handle);
        }
        
        // worker.js
        process.title = 'worker-master';
        
        const net = require("net");
        
        process.on("message",(msg,handle)=>start(handle));
        
        const buf = "hello nodejs";
        const res= ["HTTP/1.1 200 ok","content-length:"+buf.length].join("\r\n")+"\r\n\r\n"+buf;
        
        function start(server){
            server.listen();
            let num=0;
            server.onconnection = function(err,handle){
                num++;
                console.log(`worker ${process.pid} num ${num}`);
                let socket = new net.Socket({handle});
                socket.readable = socket.writable = true
                socket.end(res);
            }
        }

        運行node master.js,這里可以使用測試工具 Siege

        siege -c 20 -r 10 http://localhost:3000

        -c 并發量,并發數為20人 -r 是重復次數, 重復10次

        這種創建進程的特點是:

        • 在一個服務上同時啟動多個進程
        • 每個進程運行同樣的代碼(start方法)
        • 多個進程可以同時監聽一個端口(3000)

        不過每次請求過來交給哪個worker處理,master并不清楚,我們更希望master能夠掌控全局,將請求指定給worker,我們做下面的改造:

        //master.js
        process.title = 'node-master'
        const net =require("net");
        const {fork} = require("child_process");
        
        // 定義workers變量,保存子進程worker
        let workers = [];
        for(let i=0;i<4;i++){
            workers.push(fork("./worker.js"));
        }
        const handle = net._createServerHandle("0.0.0.0", 3000)
        handle.listen();
        // master控制請求
        handle.onconnection = function(err,handle){
            let worker = workers.pop();
            // 將請求傳遞給子進程
            worker.send({},handle);
            workers.unshift(worker);
        }
        
        // worker.js
        process.title = 'worker-master';
        const net = require("net")
        process.on("message", (msg, handle) => start(handle))
        
        const buf = "hello nodejs"
        const res = ["HTTP/1.1 200 ok", "content-length:" + buf.length].join("\r\n") + "\r\n\r\n" + buf
        
        function start(handle) {
          console.log(`get a connection on worker,pid = %d`, process.pid)
          let socket = new net.Socket({ handle })
          socket.readable = socket.writable = true
          socket.end(res)
        }

        Cluster 多進程

        Node.js 官方提供的 Cluster 模塊不僅充分利用機器 CPU 內核開箱即用的解決方案,還有助于 Node 進程增加可用性的能力,Cluster模塊是對多進程服務能力的封裝。
        // master.js
        const cluster = require("cluster");
        const numCPUS = require("os").cpus().length;
        
        if(cluster.isMaster){
            console.log(`master start...`)
            for(let i=0;i<numCPUS;i++){
                cluster.fork();
            };
        
            cluster.on("listening",(worker,address)=>{
                console.log(`master listing worker pid ${worker.process.pid} address port:${address.port}`)
            })
        
        }else if(cluster.isWorker){
            require("./wroker.js")
        }
        //wroker.js
        const http = require("http");
        http.createServer((req,res)=>res.end(`hello`)).listen(3000)

        進程重啟和守護

        進程重啟

        為了增加服務器的可用性,我們希望實例在出現崩潰或者異常退出時,能夠自動重啟。

        //master.js
        const cluster = require("cluster")
        const numCPUS = require("os").cpus().length
        
        if (cluster.isMaster) {
          console.log("master start..")
          for (let i = 0; i < numCPUS; i++) {
              cluster.fork()
            }
          cluster.on("listening", (worker, address) => {
            console.log("listening worker pid " + worker.process.pid)
          })
          cluster.on("exit", (worker, code, signal) => {
              // 子進程出現異?;蛘弑紳⑼顺?    if (code !== 0 && !worker.exitedAfterDisconnect) {
              console.log(`工作進程 ${worker.id} 崩潰了,正在開始一個新的工作進程`)
              // 重新開啟子進程
              cluster.fork()
            }
          })
        } else if (cluster.isWorker) {
          require("./server")
        }
        const http = require("http")
        const server = http.createServer((req, res) => {
            // 隨機觸發錯誤
          if (Math.random() > 0.5) {
              throw new Error(`worker error pid=${process.pid}`)
          }
          res.end(`worker pid:${process.pid} num:${num}`)
        }).listen(3000)

        如果請求拋出異常而結束子進程,主進程能夠監聽到結束事件,重啟開啟子進程。

        上面的重啟只是簡單處理,真正項目中要考慮到的就很多了,這里可以參考egg的多進程模型和進程間通訊。

        下面是來自文章Node.js進階之進程與線程更全面的例子:

        // master.js
        const {fork} = require("child_process");
        const numCPUS = require("os").cpus().length;
        
        const server = require("net").createServer();
        server.listen(3000);
        process.title="node-master";
        
        const workers = {};
        const createWorker = ()=>{
            const worker = fork("worker.js");
            worker.on("message",message=>{
                if(message.act==="suicide"){
                    createWorker();
                }
            })
        
            worker.on("exit",(code,signal)=>{
                console.log('worker process exited,code %s signal:%s',code,signal);
                delete workers[worker.pid];
            });
        
            worker.send("server",server);
            workers[worker.pid] = worker;
            console.log("worker process created,pid %s ppid:%s", worker.pid, process.ppid)
        }
        
        for (let i = 0; i < numCPUS; i++) {
          createWorker()
        }
        
        process.once("SIGINT",close.bind(this,"SIGINT")); // kill(2) Ctrl+C
        process.once("SIGQUIT", close.bind(this, "SIGQUIT")) // kill(3) Ctrl+l
        process.once("SIGTERM", close.bind(this, "SIGTERM")) // kill(15) default
        process.once("exit", close.bind(this))
        
        function close(code){
            console.log('process exit',code);
            if(code!=0){
                for(let pid in workers){
                    console.log('master process exit,kill worker pid:',pid);
                    workers[pid].kill("SIGINT");
                }
            };
            process.exit(0);
        }
        //worker.js
        const http=require("http");
        const server = http.createServer((req,res)=>{
            res.writeHead(200,{"Content-Type":"text/plain"});
            res.end(`worker pid:${process.pid},ppid:${process.ppid}`)
            throw new Error("worker process exception!");
        });
        
        let worker;
        process.title = "node-worker";
        process.on("message",(message,handle)=>{
            if(message==="server"){
                worker = handle;
                worker.on("connection",socket=>{
                    server.emit("connection",socket)
                })
            }
        })
        process.on("uncaughtException",(error)=>{
            console.log('some error')
            process.send({act:"suicide"});
            worker.close(()=>{
                console.log(process.pid+" close")
                process.exit(1);
            })
        })

        這個例子考慮更加周到,通過uncaughtException捕獲子進程異常后,發送信息給主進程重啟,并在鏈接關閉后退出。

        進程守護

        pm2可以使服務在后臺運行不受終端的影響,這里主要通過兩步處理:

        • options.detached:為true時運行子進程在父進程退出后繼續運行
        • unref() 方法可以斷絕跟父進程的關系,使父進程退出后子進程不會跟著退出
        const { spawn } = require("child_process")
        
        function startDaemon() {
          const daemon = spawn("node", ["daemon.js"], {
            // 當前工作目錄
            cwd: __dirname,
            // 作為獨立進程存在
            detached: true,
            // 忽視輸入輸出流
            stdio: "ignore",
          })
          console.log(`守護進程 ppid:%s pid:%s`, process.pid, daemon.pid)
          // 斷絕父子進程關系
          daemon.unref()
        }
        
        startDaemon()
        // daemon.js
        const fs = require("fs")
        const {Console} = require("console");
        // 輸出日志
        const logger = new Console(fs.createWriteStream("./stdout.log"),fs.createWriteStream("./stderr.log"));
        // 保持進程一直在后臺運行
        setInterval(()=>{
            logger.log("daemon pid:",process.pid,"ppid:",process.ppid)
        },1000*10);
        
        // 生成關閉文件
        fs.writeFileSync("./stop.js", `process.kill(${process.pid}, "SIGTERM")`)

        參考鏈接

        查看原文

        贊 0 收藏 0 評論 0

        認證與成就

        • 獲得 44 次點贊
        • 獲得 2 枚徽章 獲得 0 枚金徽章, 獲得 0 枚銀徽章, 獲得 2 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        (??? )
        暫時沒有

        注冊于 2017-05-01
        個人主頁被 2.5k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕