<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>

        時間被海綿吃了

        時間被海綿吃了 查看完整檔案

        北京編輯北京航空航天大學  |  軟件工程 編輯猿印  |  軟件開發 編輯 sunhengzhe.com 編輯
        編輯

        learning and coding

        個人動態

        時間被海綿吃了 回答了問題 · 2020-07-22

        解決如下,正則怎么去截取想得到的數據

        既然服務端返回的是 xml 格式的數據,那并不推薦使用正則表達式去解析,而是直接用瀏覽器提供的 DOMParser 去做,可以參考 MDN 文檔,或者使用一些封裝好的 xml 解析庫也可以。

        在題主這個場景下,基本使用如下:

        const parser = new DOMParser();
        const parsedDOM = parser.parseFromString(resp, "application/xml");
        // 北京市海淀區燕園街道北京大學
        parsedDOM.getElementsByTagName("formatted_address")[0].childNodes[0].nodeValue;

        如果實在想用正則表達式,也可以:

        resp.match(/(?<=<formatted_address>)\s*([^\s]+)\s*(?=\<\/formatted_address>)/)

        但是如上所述,并不推薦,因為可能存在性能問題,而且代碼不好維護,不如解析來得簡單明了。

        關注 5 回答 5

        時間被海綿吃了 發布了文章 · 2020-01-07

        使用裝飾器是如何構建 Nodejs 路由的

        Javascript 中的裝飾器(Decorator)是我非常喜歡的一個特性,它可以很好地提高代碼的復用性和自解釋性。雖然它目前還處在建議征集的第二階段,但在 TypeScript 里已經做為了一項實驗性特性予以支持。

        比如,我們可以用如下方式定義 Controller:

        @Controller('/cats')
        class CatsController {
          @Get()
          findAll(): string {
            return 'This action returns all cats';
          }
        
          @Get('/:id')
          findOne(): string {
            return 'This action returns a specified cat';
          }
        }

        如果熟悉 Spring Boot,會覺得這樣的定義非常親切。我們使用了 @Controller@Get 裝飾器,表示調用 /cats 返回所有的貓,調用 /cats/:id 返回按 id 查找的貓。這樣的定義形式讓代碼看上去可讀性很強,也清爽多了。

        實際上這種寫法在 TypeScript 中是比較常見的,比如 NestJs 框架就提供這種方式。

        本文簡單介紹如何使用裝飾器和反射實現這種功能。

        裝飾器概述

        在此之前,我們先回顧一下裝飾器的用法。裝飾器可以被附加到 類聲明(Class),屬性(Property), 訪問符(Accessor),方法(Method)或 參數(Parameter) 上,對應的簽名如下(其中訪問符和屬性裝飾器簽名相同):

        它們分別可以標注到對應的位置:

        @classDecorator // 類裝飾器
        class Hero {
            @propertyDecorator // 屬性裝飾器
            name: string = "";
        
            @propertyDecorator
            _hp: number = 100;
        
            @methodDecorator // 方法裝飾器
            attack(@paramDecorator enemy: Enermy /* 參數裝飾器 */) {
        
            }
        
            @propertyDecorator  // 訪問符裝飾器
            get hp() {
                return this._hp;
            }
        }

        裝飾器被調用時,第一個參數一般要么能拿到類的構造函數,要么能拿到類的原型對象,利用這個參數可以對類或者原型對象進行修改。

        反射

        Reflect 對象是 ES6 為了操作對象而提供的新 API,這里需要用到的是其中的 Metadata API,它是 ES7 的一個提案,主要用來在聲明的時候添加和讀取元數據。我們主要用到 defineMetadata 定義元數據、 hasMetadata 判斷元數據是否存在 和 getMetadata 獲取元數據。具體函數簽名見 Metadata Proposal。

        要使用 Metadata API,我們需要引用 reflect-metadata 這個庫。

        思路

        于是我們現在手上有兩樣工具,一個是裝飾器,當我們使用 @Controller、@Get、@Post 等標注在類或方法上時,我們可以獲取到類的構造函數、類的原型對象,根據裝飾器傳入的參數,能獲取到路由的路徑和請求方法。

        但我們還需使控制器可以運行,這時就可以利用反射,拿到裝飾器傳入的參數和對應的請求方法,構造出對應的路由。

        實現

        這里以 Express 框架為例,我們實現對應的裝飾器,讓 Express 可以支持裝飾器標注來添加路由,首先新建 index.ts 如下:

        import * as express from 'express';
        import { Request, Response } from 'express';
        
        const app = express();
        
        app.get('/', (req: Request, res: Response) => {
          res.send('Hello World!');
        });
        
        app.listen(3000, () => {
          console.log('Started express on port 3000');
        });

        這是一個基礎的 Express 入口文件。

        @Controller 裝飾器

        接下來實現 @Controller 裝飾器,它是標注在控制器 上的,用來標注這個類是一個控制器類,并提供一個路由前綴作為參數。因為類裝飾器第一個參數是類的構造函數,所以我們將該裝飾器傳入的前綴參數定義到構造函數的元數據中,key 為 prefix。

        // Controller.ts
        export const Controller = (prefix: string = ''): ClassDecorator => {
          return (target: Function) => {
            Reflect.defineMetadata('prefix', prefix, target);
          };
        };

        @Get 裝飾器

        @Get、@Post 等作為請求方法的裝飾器實現原理都是相似的,這里以 @Get 方法舉例,這個裝飾器應該標識請求的方式和請求的路由,另外保存被標注的函數,因為這個函數將被作為路由函數調用。

        我們首先定義一個元數據接口:

        // RouteDefinition.ts
        export interface RouteDefinition {
          path: string;
          requestMethod: 'get' | 'post' | 'delete' | 'options' | 'put';
          methodName: string;
        }

        @Get 裝飾器的實現如下:

        // Get.ts
        import {RouteDefinition} from './RouteDefinition';
        
        export const Get = (path: string): MethodDecorator => {
          return (target, propertyKey: string): void => {
            if (!Reflect.hasMetadata('routes', target.constructor)) {
              Reflect.defineMetadata('routes', [], target.constructor);
            }
        
            const routes = Reflect.getMetadata('routes', target.constructor) as Array<RouteDefinition>;
        
            routes.push({
              requestMethod: 'get',
              path,
              methodName: propertyKey
            });
            Reflect.defineMetadata('routes', routes, target.constructor);
          };
        };
        

        @Get 裝飾器是標注在方法上的,所以第一個參數是類的原型對象,我們這里還是根據它再獲取到類的構造函數,在元數據中添加一個 routes 數據,用來保存這個控制器的所有路由。

        最后,我們在 Express 的入口文件中,就可以取得所有的控制器,根據反射拿到所有的路由了。

        import 'reflect-metadata';
        
        import * as express from 'express';
        import { Request, Response } from 'express';
        import CatsController from './CatsController';
        import { RouteDefinition } from './RouteDefinition';
        
        const app = express();
        
        app.get('/', (req: Request, res: Response) => {
          res.send('Hello there!');
        });
        
        app.listen(3000, () => {
          console.log('Started express on port 3000');
        });
        
        // 構造路由
        [
          CatsController
        ].forEach(controller => {
          const instance = new controller();
          // 獲取 prefix
          const prefix = Reflect.getMetadata('prefix', controller);
          // 獲取 routes
          const routes: Array<RouteDefinition> = Reflect.getMetadata('routes', controller);
        
          routes.forEach(route => {
            // 添加 Express 路由
            app[route.requestMethod](prefix + route.path, (req: express.Request, res: express.Response) => {
              instance[route.methodName](req, res);
            });
          });
        });
        

        參考

        查看原文

        贊 1 收藏 1 評論 0

        時間被海綿吃了 發布了文章 · 2019-03-24

        Java Optional API

        一位智者說過,沒有處理過空指針異常就不算一個真正的 Java 程序員。這當然是開玩笑,但是空指針異常確實是很多程序出錯的源頭。
        于是,在 Java 8 引入了 java.util.Optional,Optional 用來代表一種 可能有可能沒有 的數據,可以用來緩解空指針異常的問題。

        簡單地說,Optional 用來避免這種代碼:

        String version = "UNKNOWN";
        if(computer != null){
          Soundcard soundcard = computer.getSoundcard();
          if(soundcard != null){
            USB usb = soundcard.getUSB();
            if(usb != null){
              version = usb.getVersion();
            }
          }
        }

        如果用 Optional 表示呢?大概是這樣:

        String version = computer.flatMap(Computer::getSoundcard)
                                 .flatMap(Soundcard::getUSB)
                                 .map(USB::getVersion)
                                 .orElse("UNKNOWN");

        實際上,Optional 即函數式編程中的 Maybe。

        以下示例在 OptionalTest.java 中。

        創建

        創建 Optional 有三種方式,分別是 empty、 of 和 ofNullable。

        empty

        empty 用來創建一個空的 Optional

        @Test
        public void create_optional_with_empty() {
            Optional<String> empty = Optional.empty();
            assertFalse(empty.isPresent());
        }

        of

        of 用來創建一個非空的 Optional:

        @Test
        public void create_optional_with_of() {
            Optional<String> java = Optional.of("Java");
            assertTrue(java.isPresent());
        }

        但是參數不能為 null,否則會拋空指針異常:

        @Test(expected = NullPointerException.class)
        public void create_optional_with_of_with_null() {
            Optional.of(null);
        }

        ofNullable

        ofNullable 用來創建一個可能為空的 Optional:

        @Test
        public void create_optional_with_ofNullable() {
            Optional<String> java = Optional.ofNullable("Java");
            assertTrue(java.isPresent());
        
            Optional<Object> o = Optional.ofNullable(null);
            assertFalse(o.isPresent());
        }

        檢測值是否存在

        可以使用 isPresentisEmpty 判斷 Optional 的值是否為空。

        isPresent

        如果 Optional 中值非 null,則返回 true,否則返回 false。

        @Test
        public void check_optional_with_isPresent() {
            Optional<String> java = Optional.ofNullable("java");
            Optional<Object> aNull = Optional.ofNullable(null);
        
            assertTrue(java.isPresent());
            assertFalse(aNull.isPresent());
        }

        isEmpty

        Java 11 開始可以使用 isEmpty。

        isEmptyisPresent 相反,如果為 null 返回 true。

        @Test
        public void check_optional_with_isEmpty() {
            Optional<String> java = Optional.ofNullable("java");
            Optional<Object> aNull = Optional.ofNullable(null);
        
            assertFalse(java.isEmpty());
            assertTrue(aNull.isEmpty());
        }

        條件動作

        關于條件的動作有 ifPresent、orElse、orElseGet、orElseThrow、or、ifPresentOrElse,它們執行與否取決于 Optional 的值是否為 null。

        為了避免空指針異常,我們會經常寫下面的代碼:

        if (name != null){
            System.out.println(name.length);
        }

        Optional 使用一種函數式的方式來替代上面的寫法。

        ifPresent

        ifPresent 接受一個 Consumer,在 Optional 值非 null 時調用,并接受 Optional 的值。

        @Test
        public void condition_action_ifPresent() {
            Optional<String> java = Optional.ofNullable("java");
            java.ifPresent((value) -> System.out.println("ifPresent accept " + value));
        
            Optional<Object> aNull = Optional.ofNullable(null);
            aNull.ifPresent(value -> System.out.println("this will never execute"));
        }

        orElse

        orElse 在 Optional 值為 null 時觸發,它接受一個參數,作為 Optional 的默認值。

        @Test
        public void condition_action_orElse() {
            assertTrue(Optional.ofNullable("java").orElse("javascript").equals("java"));
            assertTrue(Optional.ofNullable(null).orElse("java").equals("java"));
        }

        orElseGet

        orElseGet 與 orElse 類似,但 orElseGet 接受的是一個 Supplier,Supplier 返回的值作為 Optional 的默認值。

        @Test
        public void condition_action_orElseGet() {
            assertTrue(Optional.ofNullable("java").orElseGet(() -> "javascript").equals("java"));
            assertTrue(Optional.ofNullable(null).orElseGet(() -> "java").equals("java"));
        }

        orElse 和 orElseGet 的區別

        orElseorElseGet 的函數簽名是不一樣的,但如果想使用同樣的函數的返回值來作為 Optional 的默認值,我們很可能會這么干:

        public String getDefaultName() {
            System.out.println("You got a default name");
            return "default";
        }
            
        @Test
        public void difference_between_orElse_and_orElseGet() {
            Optional<String> java = Optional.of("java");
        
            System.out.println("orElse:");
            assertEquals("java", java.orElse(getDefaultName()));
            System.out.println("orElseGet:");
            assertEquals("java", java.orElseGet(this::getDefaultName));
        }

        若 java 是 null,則 orElse 和 orElseGet 沒有什么不同,getDefaultName 方法都會執行并將返回值作為 Optional 的默認值。

        當在上面的例子中,java 非 null,這時 orElse 的 getDefaultName 還是會執行,但 orElseGet 不會。輸出:

        orElse:
        You got a default name
        orElseGet:

        當 getDefaultName 中有副作用或耗時操作時需要注意。

        orElseThrow

        orElseThrow 與 orElse 一樣也在當 Optional 值為 null 時觸發,但與之不同的是會拋出指定的異常:

        @Test(expected = IllegalArgumentException.class)
        public void condition_action_orElseThrow() {
            Optional.ofNullable(null).orElseThrow(IllegalArgumentException::new);
        }

        or

        or 是 Java 9 中新增方法。與 orElseGet 很相似,or 也接受一個 Supplier,但 or 返回的是一個新的 Optional。

        @Test
        public void condition_or_optional() {
            Optional<String> java = Optional.of("java")
                                            .or(() -> Optional.of("javascript"));
            Optional<Object> java1 = Optional.empty()
                                             .or(() -> Optional.of("java"));
            assertEquals("java", java.get());
            assertEquals("java", java1.get());
        }

        ifPresentOrElse

        ifPresentOrElse 是 Java 9 中新增的方法。ifPresent 就如同命令式編程中的 if-else,它接受兩個參數,第一個為 Consumer,在 Optional 有值時調用,第二個為 Runnable,在無值時調用:

        @Test
        public void condition_ifPresentOrElse() {
            // value is java
            Optional.of("java")
                    .ifPresentOrElse(value -> System.out.println("value is " + value), () -> System.out.println("ooops"));
        
            // ooops
            Optional.empty()
                    .ifPresentOrElse(value -> System.out.println("value is " + value), () -> System.out.println("ooops"));
        }

        獲取值

        Optional 提供了一個 get 方法獲取值,但 get 方法只能在 Optional 有值時使用,否則會拋出 NoSuchElementException 異常:

        @Test
        public void get_optional_with_of() {
            Optional<String> java = Optional.of("Java");
            assertEquals("java", java.get());
        }
        
        @Test(expected = NoSuchElementException.class)
        public void get_optional_with_of_with_null() {
            Optional.empty().get();
        }

        驗證值

        filter 方法用來驗證 Optional 的值是否符合條件,它接受一個 Predicate 作為參數。如果 Optional 的值為 null 或 Predicate 判斷不通過,則返回 empty;否則返回該 Optional。

        @Test
        public void test_optional_by_filter() {
            Integer nullYear = null;
            Optional<Integer> integer = Optional.ofNullable(nullYear)
                                                .filter(value -> value == 2018);
            assertEquals(Optional.empty(), integer);
        
            Integer year = 2019;
            Optional<Integer> integer1 = Optional.ofNullable(year)
                                                 .filter(value -> value == 2018);
            assertEquals(Optional.empty(), integer1);
        
            Optional<Integer> integer2 = Optional.ofNullable(year)
                                                 .filter(value -> value == 2019);
            assertEquals("Optional[2019]", integer2.toString());
        }

        filter 相對傳統 if 而言省去了很多樣板代碼,如:

        public boolean priceIsInRange1(Modem modem) {
            boolean isInRange = false;
         
            if (modem != null && modem.getPrice() != null
              && (modem.getPrice() >= 10
                && modem.getPrice() <= 15)) {
         
                isInRange = true;
            }
            return isInRange;
        }

        使用 Optional 實現同樣的方法:

        public boolean priceIsInRange2(Modem modem2) {
             return Optional.ofNullable(modem2)
               .map(Modem::getPrice)
               .filter(p -> p >= 10)
               .filter(p -> p <= 15)
               .isPresent();
        }

        處理值

        處理值的方式有 map 和 flatMap。

        map

        使用 map 可以對 Optional 中的值進行處理并返回。

        @Test
        public void map_optional() {
            Optional<String> java = Optional.of("java");
            String result = java.map(String::toUpperCase).orElse("");
            assertEquals("JAVA", result);
        }

        flatMap

        flatMap 與 map 的區別在于 map 處理值后會包裝返回值,而 flatMap 不包裝。

        public class Person {
            private String name;
        
            public Person(String name) {
                this.name = name;
            }
        
            public Optional<String> getName() {
                return Optional.ofNullable(name);
            }
        }
        
        @Test
        public void flatMap_optional() {
            Person person = new Person("john");
            Optional<Person> personOptional = Optional.of(person);
        
            String byMap = personOptional.map(Person::getName)
                                         // 需要手動打開包裝
                                         .orElse(Optional.empty())
                                         .orElse("");
        
            String byFlatMap = personOptional.flatMap(Person::getName)
                                             .orElse("");
        
            assertEquals("john", byMap);
            assertEquals("john", byFlatMap);
        }

        流操作

        在 Java 9 中,新增了 stream 方法,可以對 Optional 創建 stream,然后可以使用 stream 上的所有方法。

        如果 Optional 為 empty,則創建一個 empty 的 stream。

        @Test
        public void treat_optional_as_stream() {
            List<String> collect = Optional.of("java")
                                           .stream()
                                           .map(value -> value.concat("script"))
                                           .collect(Collectors.toList());
        
            assertArrayEquals(new String[]{"javascript"}, collect.toArray());
        
        
            // empty optional
            Optional<String> value = Optional.empty();
            List<String> emptyStream = value.stream()
                                            .map(String::toUpperCase)
                                            .collect(Collectors.toList());
        
            assertEquals(0, emptyStream.size());
        }

        所以使用 stram 也可以篩出非 null 的 Optional 的值:

        @Test
        public void filter_empty_by_stream() {
            List<Optional<String>> languages = List.of(Optional.of("java"), Optional.empty(), Optional.empty(), Optional.of("javascript"));
            List<String> collect = languages.stream()
                                            .flatMap(Optional::stream)
                                            .collect(Collectors.toList());
        
            assertArrayEquals(new String[]{"java", "javascript"}, collect.toArray());
        }

        參考

        查看原文

        贊 0 收藏 0 評論 0

        時間被海綿吃了 發布了文章 · 2019-02-23

        實現簡單的正則表達式引擎

        回想起第一次看到正則表達式的時候,眼睛里大概都是 $7^(0^=]W-\^*d+,心里我是拒絕的。不過在后面的日常工作里,越來越多地開始使用到正則表達式,正則表達式也逐漸成為一個很常用的工具。

        要掌握一種工具除了了解它的用法,了解它的原理也是同樣重要的,一般來說,正則引擎可以粗略地分為兩類:DFA(Deterministic Finite Automata)確定性有窮自動機和 NFA (Nondeterministic Finite Automata)不確定性有窮自動機。

        使用 NFA 的工具包括 .NET、PHP、Ruby、Perl、Python、GNU Emacs、ed、sec、vi、grep 的多數版本,甚至還有某些版本的 egrepawk。而采用 DFA 的工具主要有 egrep、awk、lexflex。也有些系統采用了混合引擎,它們會根據任務的不同選擇合適的引擎(甚至對同一表達式中的不同部分采用不同的引擎,以求得功能與速度之間的最佳平衡)。—— Jeffrey E.F. Friedl《精通正則表達式》

        DFA 與 NFA 都稱為有窮自動機,兩者有很多相似的地方,自動機本質上是與狀態轉換圖類似的圖。(注:本文不會嚴格給自動機下定義,深入理解自動機可以閱讀《自動機理論、語言和計算導論》。)

        NFA

        一個 NFA 分為以下幾個部分:

        • 一個初始狀態
        • 一個或多個終結狀態
        • 狀態轉移函數

        clipboard.png

        上圖是一個具有兩個狀態 q0q1 的 NFA,初始狀態為 q0(沒有前序狀態),終結狀態為 q1(兩層圓圈標識)。在 q0 上有一根箭頭指向 q1,這代表當 NFA 處在 q0 狀態時,接受輸入 a,會轉移到狀態 q1。

        當要接受一個串時,我們會將 NFA 初始化為初始狀態,然后根據輸入來進行狀態轉移,如果輸入結束后 NFA 處在結束狀態,那就意味著接受成功,如果輸入的符號沒有對應的狀態轉移,或輸入結束后 NFA 沒有處在結束狀態,則意味著接受失敗。

        由上可知這個 NFA 能接受且僅能接受字符串 a。

        那為什么叫做 NFA 呢,因為 對于同一個狀態與同一個輸入符號,NFA 可以到達不同的狀態,如下圖:

        clipboard.png

        q0 上時,當輸入為 a,該 NFA 可以繼續回到 q0 或者到達 q1,所以該 NFA 可以接受 abbq0 -> q1 -> q2 -> q3),也可以接受 aabbq0 -> q0 -> q1 -> q2 -> q3),同樣接受 ababb、aaabbbabababb 等等,你可能已經發現了,這個 NFA 表示的正則表達式正是 (a|b)*abb

        ε-NFA

        除了能到達多個狀態之外,NFA 還能接受空符號 ε,如下圖:

        clipboard.png

        這是一個接受 (a+|b+) 的 NFA,因為存在路徑 q0 -ε-> q1 -a-> q2 -a-> q2,ε 代表空串,在連接時移除,所以這個路徑即代表接受 aa。

        你可能會覺得為什么不直接使用 q0 通過 a 連接 q2,通過 b 連接到 q4,這是因為 ε 主要起連接的作用,介紹到后面會感受到這點。

        DFA

        介紹完了不確定性有窮自動機,確定性有窮自動機就容易理解了,DFA 和 NFA 的不同之處就在于:

        • 沒有 ε 轉移
        • 對于同一狀態和同一輸入,只會有一個轉移

        那么 DFA 要比 NFA 簡單地多,為什么不直接使用 DFA 實現呢?這是因為對于正則語言的描述,構造 NFA 往往要比構造 DFA 容易得多,比如上文提到的 (a|b)*abb,NFA 很容易構造和理解:

        clipboard.png

        但直接構造與之對應的 DFA 就沒那么容易了,你可以先嘗試構造一下,結果大概就是這樣:

        clipboard.png

        所以 NFA 容易構造,但是因為其不確定性很難用程序實現狀態轉移邏輯;DFA 不容易構造,但是因為其確定性很容易用程序來實現狀態轉移邏輯,怎么辦呢?

        神奇的是每一個 NFA 都有對應的 DFA,所以我們一般會先根據正則表達式構建 NFA,然后可以轉化成對應的 DFA,最后進行識別。

        McMaughton-Yamada-Thompson 算法

        McMaughton-Yamada-Thompson 算法可以將任何正則表達式轉變為接受相同語言的 NFA。它分為兩個規則:

        基本規則

        1. 對于表達式 ε,構造下面的 NFA:
          clipboard.png
        2. 對于非 ε,構造下面的 NFA:
          clipboard.png

        歸納規則

        假設正則表達式 s 和 t 的 NFA 分別為 N(s)N(t),那么對于一個新的正則表達式 r,則如下構造 N(r)

        r = s|t,N(r)

        clipboard.png

        連接

        r = st,N(r)

        clipboard.png

        閉包

        r = s*,N(r)

        clipboard.png

        其他的 +,? 等限定符可以類似實現。本文所需關于自動機的知識到此就結束了,接下來就可以開始構建 NFA 了。

        基于 NFA 實現

        1968 年 Ken Thompson 發表了一篇論文 Regular Expression Search Algorithm,在這篇文章里,他描述了一種正則表達式編譯器,并催生出了后來的 qed、ed、grepegrep。論文相對來說比較難懂,implementing-a-regular-expression-engine 這篇文章同樣也是借鑒 Thompson 的論文進行實現,本文一定程度也參考了該文章的實現思路。

        添加連接符

        在構建 NFA 之前,我們需要對正則表達式進行處理,以 (a|b)*abb 為例,在正則表達式里是沒有連接符號的,那我們就沒法知道要連接哪兩個 NFA 了。

        所以首先我們需要顯式地給表達式添加連接符,比如 ·,可以列出添加規則:

        左邊符號 / 右邊符號*()字母
        *?????
        (?????
        )?????
        ?????
        字母?????

        (a|b)*abb 添加完則為 (a|b)*·a·b·b,實現如下:

        clipboard.png

        中綴表達式轉后綴表達式

        如果你寫過計算器應該知道,中綴表達式不利于分析運算符的優先級,在這里也是一樣,我們需要將表達式從中綴表達式轉為后綴表達式。

        在本文的具體過程如下:

        1. 如果遇到字母,將其輸出。
        2. 如果遇到左括號,將其入棧。
        3. 如果遇到右括號,將棧元素彈出并輸出直到遇到左括號為止。左括號只彈出不輸出。
        4. 如果遇到限定符,依次彈出棧頂優先級大于或等于該限定符的限定符,然后將其入棧。
        5. 如果讀到了輸入的末尾,則將棧中所有元素依次彈出。

        在本文實現范圍中,優先級從小到大分別為

        • 連接符 ·
        • 閉包 *
        • |

        實現如下:

        clipboard.png

        (a|b)*·c 轉為后綴表達式 ab|*c·

        構建 NFA

        由后綴表達式構建 NFA 就容易多了,從左到右讀入表達式內容:

        • 如果為字母 s,構建基本 NFA N(s),并將其入棧
        • 如果為 |,彈出棧內兩個元素 N(s)、N(t),構建 N(r) 將其入棧(r = s|t
        • 如果為 ·,彈出棧內兩個元素 N(s)、N(t),構建 N(r) 將其入棧(r = st
        • 如果為 *,彈出棧內一個元素 N(s),構建 N(r) 將其入棧(r = s*

        代碼見 automata.ts

        構建 DFA

        有了 NFA 之后,可以將其轉為 DFA。NFA 轉 DFA 的方法可以使用 子集構造法,NFA 構建出的 DFA 的每一個狀態,都是包含原始 NFA 多個狀態的一個集合,比如原始 NFA 為

        clipboard.png

        這里我們需要使用到一個操作 ε-closure(s),這個操作代表能夠從 NFA 的狀態 s 開始只通過 ε 轉換到達的 NFA 的狀態集合,比如 ε-closure(q0) = {q0, q1, q3},我們把這個集合作為 DFA 的開始狀態 A。

        那么 A 狀態有哪些轉換呢?A 集合里有 q1 可以接受 a,有 q3 可以接受 b,所以 A 也能接受 ab。當 A 接受 a 時,得到 q2, 那么 ε-closure(q2) 則作為 A 狀態接受 a 后到達的狀態 B。同理,A 狀態接受 b 后到達的 ε-closure(q4) 為狀態 C。

        而狀態 B 還可以接受 a,到達的同樣是 ε-closure(q2),那我們說狀態 B 接受 a 還是到達了狀態 B。同樣,狀態 C 接受 b 也會回到狀態 C。這樣,構造出的 DFA 為

        clipboard.png

        DFA 的開始狀態即包含 NFA 開始狀態的狀態,終止狀態亦是如此。

        搜索

        其實我們并不用顯式構建 DFA,而是用這種思想去遍歷 NFA,這本質上是一個圖的搜索,實現代碼如下:

        clipboard.png

        getClosure 代碼如下:

        clipboard.png

        總結

        總的來說,基于 NFA 實現簡單的正則表達式引擎,我們一共經過了這么幾步:

        1. 添加連接符
        2. 轉換為后綴表達式
        3. 構建 NFA
        4. 判斷 NFA 是否接受輸入串

        完整代碼見 github

        查看原文

        贊 2 收藏 0 評論 0

        時間被海綿吃了 贊了文章 · 2019-02-14

        宇宙最強vscode教程(基礎篇)

        本文主要介紹vscode在工作中常用的快捷鍵及插件,目標在于提高工作效率

        本文的快捷鍵是基于mac的,windows下的快捷鍵放在括號里 Cmd+Shift+P(win Ctrl+Shift+P)

        [TOC]

        零、快速入門

        有經驗的可以跳過快速入門或者大致瀏覽一遍

        1. 命令面板

        命令面板是vscode快捷鍵的主要交互界面,可以使用f1或者Cmd+Shift+P(win Ctrl+Shift+P)打開。

        在命令面板中你可以輸入命令進行搜索(中英文都可以),然后執行。

        命名面板中可以執行各種命令,包括編輯器自帶的功能和插件提供的功能。

        所以一定要記住它的快捷鍵Cmd+Shift+P

        image-20190120143658078

        2. 界面介紹

        剛上手使用vscode時,建議要先把它當做一個文件編輯器(可以打字然后保存),等到有了一定經驗再去熟悉那些快捷鍵

        先來熟悉一下界面及快捷命令(不用記)

        3. 在命令行中使用vscode

        如果你是 Windows用戶,安裝并重啟系統后,你就可以在命令行中使用 code 或者 code-insiders了,如果你希望立刻而不是等待重啟后使用,可以將 VS Code 的安裝目錄添加到系統環境變量 PATH

        如果你是mac用戶,安裝后打開命名面板Cmd+Shift+P,搜索shell命令,點擊在PAth中安裝code命令,然后重啟終端就ok了

        image-20190120144757840

        最基礎的使用就是使用code命令打開文件或文件夾

        code 文件夾地址,vscode 就會在新窗口中打開該文件夾

        如果你希望在已經打開的窗口打開文件,可以使用-r參數

        vscode命令還有其他功能,比如文件比較,打開文件跳轉到指定的行和列,如有需要自行百度:bowing_woman:

        注意:

        在繼續看文章之前記住記住打開命令面板的快捷鍵Cmd+shift+P(win下是Ctrl+shift+p)

        一、代碼編輯

        windows下的快捷鍵放在括號里

        光標的移動

        基礎

        1. 移動到行首 Cmd+左方向鍵 (win Home)
        2. 移動到行尾 Cmd+右方向鍵 (win End)
        3. 移動到文檔的開頭和末尾 Cmd+上下方向鍵 (win Ctrl+Home/End)
        4. 在花括號{}左邊右邊之間跳轉 Cmd+Shift+ (win Ctrl+Shift+)

        進階

        1. 回到上一個光標的位置,Cmd+U(win Ctrl+U) 非常有用,有時候vue文件,你改了html,需要去下面改js,改完js又需要回去,這時候Cmd+U直接回
        2. 在不同的文件之間回到上一個光標的位置 Control+- (win 沒測試,不知道),你改了a文件,改了b文件之后想回到a文件繼續編輯,mac使用controls+-

        文本選擇

        1. 你只需要多按一個shift鍵就可以在光標移動的時候選中文本
        2. 選中單詞 Cmd+D 下面要講的多光標也會講到Cmd+D
        3. 對于代碼塊的選擇沒有快捷鍵,可以使用cmd+shift+p打開命令面板,輸入選擇括號所有內容,待會說下如何添加快捷鍵

        1

        刪除

        1. 你可以選中了代碼之后再刪除,再按Backpack(是backpack嗎)或者delete刪除,但是那樣做太low了
        2. 所以,最Geek的刪除方式是Cmd+Shift+K (win Ctrl+Shift+K),想刪多少刪多少,當前你可以使用ctrl+x剪切,效果一樣的

        2

        代碼移動

        • Option+上下方向鍵(win Alt+上下)

        3

        • 代碼移動的同時按住shift就可以實現代碼復制 Option+Shift+上下3

        添加注釋

        注釋有兩種形式,單行注釋和塊注釋(在js中,單行注釋//,塊注釋/**/)

        • 單行注釋 Cmd+/ (win Ctrl +/)
        • 塊注釋 Option+Shift+A

        注意:不同語言使用的注釋不同

        二、代碼格式

        代碼格式化

        • 對整個文檔進行格式化:Option+Shift+F (win Alt+Shift+F),vscode會根據你使用的語言,使用不同的插件進行格式化,記得要下載相應格式化的插件
        • 對選中代碼進行格式化: Cmd+K Cmk+F win(Ctrl+K Ctrl+F)

        代碼縮進

        • 真個文檔進行縮進調節,使用Cmd+Shift+P打開命令面板,輸入縮進,然后選擇相應的命令
        • 選中代碼縮進調節:Cmd+] Cmd+[ 分別是減小和增加縮進(win 下不知道,自行百度)

        三、一些小技巧

        • 調整字符的大小寫,選中,然后在命令面板輸入轉化為大寫或者轉化為小寫

        • 合并代碼行,多行代碼合并為一行,Cmd+J(win下未綁定)

        • 行排序,將代碼行按照字母順序進行排序,無快捷鍵,調出命令面板,輸入按升序排序或者按降序排序

        四、多光標特性

        使用鼠標:

        按住Option(win Alt),然后用鼠標點,鼠標點在哪里哪里就會出現一個光標

        注意:有的mac電腦上是按住Cmd,然后用鼠標點才可以

        快捷命令

        1. Cmd+D (win Ctrl+D) 第一次按下時,它會選中光標附近的單詞;第二次按下時,它會找到這個單詞第二次出現的位置,創建一個新的光標,并且選中它。(注:cmd-k cmd-d 跳過當前的選擇)

        2. Option+Shift+i (win Alt+Shift+i) 首先你要選中多行代碼,然后按Option+Shift+i,這樣做的結果是:每一行后面都會多出來一個光標

        撤銷多光標

        • 使用Esc 撤銷多光標
        • 鼠標點一下撤銷

        五、快速跳轉(文件、行、符號)

        快速打開文件

        Cmd+P (win Ctrl+P)輸入你要打開的文件名,回車打開

        這里有個小技巧,選中你要打開的文件后,按Cmd+Enter,就會在一個新的編輯器窗口打開(窗口管理,見下文)

        在tab不同的文件間切換,cmd+shift+[]

        行跳轉

        加入瀏覽器報了個錯,錯誤在53行,如何快速跳轉到53行

        Ctrl+g 輸入行號

        如果你想跳轉到某個文件的某一行,你只需要先按下 “Cmd + P”,輸入文件名,然后在這之后加上 “:”和指定行號即可。

        符號跳轉

        符號可以是文件名、函數名,可以是css的類名

        Cmd+Shift+O(win Ctrl+Shift+o) 輸入你要跳轉的符號,回車進行跳轉

        win下輸入Ctrl+T,可以在不同文件的符號間進行搜索跳轉

        定義(definition)和實現(implementation)處

        f12跳到函數的定義處

        Cmd+f12(win Ctrl+f12)跳轉到函數的實現處

        引用跳轉

        很多時候,除了要知道一個函數或者類的定義和實現以外,你可能還希望知道它們被誰引用了,以及在哪里被引用了。這時你只需要將光標移動到函數或者類上面,然后按下 Shift + F12,VS Code 就會打開一個引用列表和一個內嵌的編輯器。在這個引用列表里,你選中某個引用,VS Code 就會把這個引用附近的代碼展示在這個內嵌的編輯器里。

        六、代碼重構

        當我們想修改一個函數或者變量的名字時候,我們只需把光標放到函數或者變量名上,然后按下 F2,這樣這個函數或者變量出現的地方就都會被修改。

        查看原文

        贊 457 收藏 340 評論 19

        時間被海綿吃了 關注了問題 · 2019-02-13

        正則表達式內存溢出問題

        問題描述

        正則表達式內存溢出,JVM無法是否內存,一直累加,然后程序掛掉

        問題出現的環境背景及自己嘗試過哪些方法

        把第二組括號刪了,就沒事了。。。無語
        這個代碼是用來匹配下列字母
        AB ABABABA ABABABA ABABABA
        前2個字母是一組,中間的是一組,最后的是一組
        但是,使用下列代碼,程序運行會導致,內存一直累加無法釋放。。。

        相關代碼

        // 請把代碼文本粘貼到下方(請勿用圖片代替代碼)
        "^([A-Z]{2})\s((\w{7}\s)*)(\w{7})$"
        matcher.group(1)
        matcher.group(2)
        matcher.group(4)
        把正則表達式第二組括號刪了,就沒事了。。。無語

        你期待的結果是什么?實際看到的錯誤信息又是什么?

        關注 3 回答 0

        時間被海綿吃了 發布了文章 · 2019-02-13

        Canvas 文本轉粒子效果

        clipboard.png

        通過粒子來繪制文本讓人感覺很有意思,配合粒子的運動更會讓這個效果更加酷炫。本文介紹在 canvas 中通過粒子來繪制文本的方法。

        實現原理

        總的來說要做出將文本變成粒子展示的效果其實很簡單,實現的原理就是使用兩張 canvas,一張是用戶看不到的 A canvas,用來繪制文本;另一張是用戶看到的 B canvas,用來根據 A 的文本數據來生成粒子。直觀表示如圖:

        clipboard.png

        創建離屏 canvas

        HTML 只需要放置主 canvas 即可:

        <!-- HTML 結構 -->
        <html>
        <head>
          ...
        </head>
        <body>
          <canvas id="stage"></canvas>
        </body>
        </html>

        然后創建一個離屏 canvas,并繪制文本:

        const WIDTH = window.innerWidth;
        const HEIGHT = window.innerHeight;
        
        const offscreenCanvas = document.createElement('canvas');
        const offscreenCtx = offscreenCanvas.getContext('2d');
        
        offscreenCanvas.width = WIDTH;
        offscreenCanvas.height = HEIGHT;
        
        offscreenCtx.font = '100px PingFang SC';
        offscreenCtx.textAlign = 'center';
        offscreenCtx.baseline = 'middle';
        offscreenCtx.fillText('Hello', WIDTH / 2, HEIGHT / 2);

        這時頁面上什么也沒有發生,但實際上可以想象在離屏 canvas 上,此時應該如圖所示:

        clipboard.png

        核心方法 getImageData

        使用 canvas 的 getImageData 方法,可以獲取一個 ImageData 對象,這個對象用來描述 canvas 指定區域內的像素數據。也就是說,我們可以獲取 “Hello” 這個文本每個像素點的位置和顏色,也就可以在指定位置生成粒子,最后形成的效果就是粒子拼湊成文本了。

        要獲取像素信息,需要使用 ImageData 對象的 data 屬性,它將所有像素點的 rgba 值鋪開成了一個數組,每個像素點有 rgba 四個值,這個數組的個數也就是 像素點數量 * 4。

        假設我選取了一個 3 * 4 區域,那么一共 12 個像素點,每個像素點有 rgba 四個值,所以 data 這個數組就會有 12 * 4 = 48 個元素。

        clipboard.png

        如果打印出 data,可以看到即從左往右,從上往下排列這些像素點的 rgba。

        clipboard.png

        當然我們要獲取的區域必須要包含文本,所以應該獲取整個離屏 canvas 的區域:

        const imgData = offscreenCtx.getImageData(0, 0, WIDTH, HEIGHT).data;

        生成粒子

        拿到 ImageData 后,通過遍歷 data 數組,可以判斷在離屏 canvas 的畫布中,哪些點是有色彩的(處于文本中間),哪些點是沒有色彩的(不在文本上),把那些有色彩的像素位置記下來,然后在主 canvas 上生成粒子,就 ok 了。

        首先創建一下粒子類:

        class Particle {
            constructor (options = {}) {
                const { x = 0, y = 0, color = '#fff', radius = 5} = options;
                this.radius = radius;
                this.x = x;
                this.y = y;
                this.color = color;
            }
        
            draw (ctx) {
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
                ctx.fillStyle = this.color;
                ctx.fill();
                ctx.closePath();
            }
        }

        遍歷 data,我們可以根據透明度,也就是 rgba 中的第四個元素是否不為 0 來判斷該像素是否在文本中。

        const particles = [];
        const skip = 4;
        for (var y = 0; y < HEIGHT; y += skip) {
            for (var x = 0; x < WIDTH; x += skip) {
                var opacityIndex = (x + y * WIDTH) * 4 + 3;
                if (imgData[opacityIndex] > 0) {
                    particles.push(new Particle({
                        x,
                        y,
                        radius: 1,
                        color: '#2EA9DF'
                    }));
                }
            }
        }

        我們用 particles 數組來存放所有的粒子,這里的 skip 的作用是遍歷的步長,如果我們一個像素一個像素地掃,那么最后拼湊文本的粒子將會非常密集,增大這個值,最后產生的粒子就會更稀疏。

        最后在創建主 canvas 并繪制即可:

        const canvas = document.querySelector('#stage');
        canvas.width = WIDTH;
        canvas.height = HEIGHT;
        const ctx = canvas.getContext('2d');
        
        for (const particle of particles) {
            particle.draw(ctx);
        }

        效果如下:

        clipboard.png

        完整代碼見 01-basic-text-to-particles

        添加效果

        了解實現原理之后,其實其他的就都是給粒子添加一些動效了。首先可以讓粒子有一些隨機的位移,避免看上去過于整齊。

        const particles = [];
        const skip = 4;
        for (var y = 0; y < HEIGHT; y += skip) {
            for (var x = 0; x < WIDTH; x += skip) {
                var opacityIndex = (x + y * WIDTH) * 4 + 3;
                if (imgData[opacityIndex] > 0) {
                    // 創建粒子時加入隨機位移
                    particles.push(new Particle({
                        x: x + Math.random() * 6 - 3,
                        y: y + Math.random() * 6 - 3,
                        radius: 1,
                        color: '#2EA9DF'
                    }));
                }
            }
        }

        效果如下:

        clipboard.png

        如果想實現變大的效果,如:

        圖片描述

        這種要怎么實現呢,首先需要隨機產生粒子的大小,這只需要在創建粒子時對 radius 進行 random 即可。另外如果要讓粒子半徑動態改變,那么需要區分開粒子的渲染半徑和初始半徑,并使用 requestAnimationFrame 進行動畫渲染:

        class Particle {
            constructor (options = {}) {
                const { x = 0, y = 0, color = '#fff', radius = 5} = options;
                this.radius = radius;
                // ...
                this.dynamicRadius = radius; // 添加 dynamicRadius 屬性
            }
        
            draw (ctx) {
                // ...
                ctx.arc(this.x, this.y, this.dynamicRadius, 0, 2 * Math.PI, false); // 替換為 dynamicRadius
                // ...
            }
            
            update () {
                // TODO
            }
        }
        
        requestAnimationFrame(function loop() {
            requestAnimationFrame(loop);
        
            ctx.fillStyle = '#fff';
            ctx.fillRect(0, 0, WIDTH, HEIGHT);
        
            for (const particle of particles) {
                particle.update();
                particle.draw(ctx);
            }
        });

        那么關鍵就在于粒子的 update 方法要如何實現了,假設我們想讓粒子半徑在 1 到 5 中平滑循環改變,很容易讓人聯想到三角函數,如:

        clipboard.png

        橫軸應該是與時間相關,可以再維護一個變量每次調用 update 的時候進行加操作,簡單做也可以直接用時間戳來進行計算。update 方法示例如下:

        update () {
            this.dynamicRadius = 3 + 2 * Math.sin(new Date() / 1000 % 1000 * this.radius);
        }

        完整代碼見 02-text-to-particles-with-size-changing

        查看原文

        贊 1 收藏 1 評論 0

        時間被海綿吃了 發布了文章 · 2019-01-24

        Docker: 容器互訪的三種方式

        場景

        三個容器

        • digger-app: 啟動 API 服務,依賴 redismysql
        • digger-redis: redis 服務
        • digger-mysql: mysql 服務

        我們需要讓 digger-app 容器內運行的服務能夠訪問 digger-redisdigger-mysql 容器。

        方法一:--link

        --link 的格式為 --link name:alias,name 為需要連接到的容器的 name,alias 是給這個連接取個別名。

        首先啟動 redis 服務和 mysql 服務:

        # redis
        docker run --name digger-redis -d redis:5.0.3-alpine
        # mysql
        docker run --name digger-mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7.25

        如果不指定 name,docker 會隨機生成一個 name,使用 docker ps 可以查看到運行容器的 name。

        在啟動 digger-app 時,指定 --link 參數:

        docker run --name digger-api --link digger-redis:redis --link digger-mysql:mysql -d -p 3000:3000 your-image

        這樣在 digger-api 中就能通過 連接名 訪問到對應的服務了,如與 digger-redislink 別名為 redis,那么在 digger-api 代碼中,可以指定 redishostredis,以 node.js 舉例:

        // redis.js
        const redis = require('redis');
        const client = redis.createClient({
          host: 'redis',
          port: 6379
        });
        
        // mysql.js
        const mysql      = require('mysql');
        const connection = mysql.createConnection({
          host     : 'mysql',
          user     : 'root',
          password : 'root',
          database : 'my_db'
        });
        
        connection.connect();

        使用 docker exec 命令進入容器,使用 ping 命令也可以查看容器是否互聯成功:

        clipboard.png

        事實上,在 digger-api 容器內,如果查看 hosts 文件,可以發現 docker 已經將另外兩個容器配置在了 hosts 中:

        /app # cat /etc/hosts
        127.0.0.1    localhost
        ...
        172.17.0.6    redis 7a6409598773 cache-redis
        172.17.0.5    mysql f08bf0e0bf18 digger-mysql
        172.17.0.7    6eb8dab1e6db

        方法二:--network

        隨著 Docker 網絡的完善,更建議將容器加入自定義的 Docker 網絡來連接多個容器,而不是使用 --link 參數。

        使用 --network 命令可以指定容器運行的網絡,通過將多個容器指定到同一個網絡可以讓容器間相互訪問。

        創建網絡

        docker network create -d bridge my-net

        指定網絡

        # redis
        docker run --name digger-redis -d --network my-net redis:5.0.3-alpine
        # mysql
        docker run --name digger-mysql -e MYSQL_ROOT_PASSWORD=root -d --network my-net mysql:5.7.25
        # api
        docker run --name digger-api --network my-net -d -p 3000:3000 your-image

        不過需要注意這時候就沒有連接的別名了,在容器里面,host 直接使用對方容器的 name 訪問即可。

        方法三:docker compose

        Docker ComposeDocker 官方編排(Orchestration)項目之一,負責快速的部署分布式應用。

        新建 docker-compose.yml 文件,編寫如下

        version: "3"
        services:
        
          digger-api:
            image: "express:v1"
            ports:
              - "3000:3000"
        
          digger-mysql:
            image: "mysql:5.7.25"
            environment:
              - MYSQL_ROOT_PASSWORD=root
        
          digger-redis:
            image: "redis:5.0.3-alpine"

        docker compose 的官方文檔查看 這里。

        然后使用 docker-compose up -d 啟動即可,容器會在后臺運行。

        查看原文

        贊 11 收藏 6 評論 1

        時間被海綿吃了 發布了文章 · 2019-01-21

        Jenkins + Docker 簡單部署 node.js 項目

        有了前幾篇的基礎后,我們現在已經能

        docker 篇

        • 構建 docker 鏡像
        • 上傳私有倉庫
        • 拉取私有鏡像
        • 啟動容器

        jenkins 篇

        • 配置 pipeline
        • 觸發 pipeline

        接下來就可以結合兩者,用 jenkins + docker 來自動化部署我們的項目。

        配置 Jenkins

        jenkins 的配置思路為

        1. 構建機(IP: xx.xx.xx.xx)拉取代碼
        2. 構建機安裝依賴
        3. 構建機運行測試
        4. 構建機打包并上傳鏡像至私有鏡像倉庫
        5. 部署機(IP: yy.yy.yy.yy)拉取鏡像
        6. 部署機重啟服務

        對應 pipeline 配置如下

        pipeline {
                agent any
                stages {
                        stage('Update') {
                                steps {
                                        sh """
                                        npm install
                                        """
                                }
                        }
                        
                        stage('Test') {
                                steps {
                                        sh "npm test"
                                }
                        }
                        
                        stage('Build') {
                                steps {
                                        sh """
                        docker build -t localhost:5000/wool-digger-api:$BUILD_NUMBER .
                                        docker push localhost:5000/wool-digger-api:$BUILD_NUMBER
                        """
                                }
                        }
                        
                        stage('Deploy') {
                                steps {
                                        sh """
                                        ssh -o stricthostkeychecking=no root@xx.xx.xx.xx "
                                          source /etc/profile
                                          docker pull yy.yy.yy.yy:5000/wool-digger-api:$BUILD_NUMBER
                                          docker rm -f wool-digger-api
                                          docker run -d --name=wool-digger-api --network host yy.yy.yy.yy:5000/wool-digger-api:$BUILD_NUMBER
                                        "
                                        """
                                }
                        }
                }
        }
        

        BULID_NUMBER

        BuildDeploy 環節里,使用了 $BUILD_NUMBER 這個變量來作為鏡像的 tag,這個變量是 jenkins 的系統變量之一,代表當前的構建號,每次構建這個號會加一,所以可以作為我們鏡像的 tag。其他系統變量可 在此查看。

        Network

        這里使用 docker run 命令的時候,加入了 --network 參數,這個參數用來指定 Docker 容器運行的網絡,默認為 bridge,即橋接模式。這種模式下在容器內通過 localhost 是訪問不到宿主機的。

        如果指定為 host 則容器與宿主機共用網絡,就無需使用 -p 命令映射端口了。這種模式下會破話隔離性,這里是為了在容器內方便地連接宿主機的 mysqlredis,推薦將 mysqlredis 也使用 docker 運行,host 值可作為一種臨時解決方案。

        配置 Docker

        docker 的配置無需做太多修改

        FROM node:10.15.0-alpine
        MAINTAINER sunhengzhe@foxmail.com
        COPY . /app/
        WORKDIR /app
        RUN npm install pm2 -g
        EXPOSE 1337
        CMD ["pm2-runtime", "pm2/production.json"]

        這里的基本鏡像使用了 node 的 alpine 版本,alpine 是面向安全的輕型 Linux 發行版,它的體積非常小。目前 Docker 官方已開始推薦使用 Alpine 替代之前的 Ubuntu 做為基礎鏡像環境。這樣會帶來多個好處。包括鏡像下載速度加快,鏡像安全性提高,主機之間的切換更方便,占用更少磁盤空間等。

        其他

        刪除鏡像

        如果需要批量刪除鏡像,可以使用

        docker rmi $(docker images | grep '鏡像名' | awk '{print $3}') 

        持久化日志

        如上篇提到的,可以通過 -v 掛載容器內日志目錄到宿主機。

        查看原文

        贊 0 收藏 0 評論 0

        時間被海綿吃了 發布了文章 · 2019-01-20

        Docker: 上傳鏡像至私有倉庫

        鏡像可以很方便直接 push 到 docker 的公共倉庫,就好像 github 一樣,但是我們在開發中很多時候都不想公開鏡像文件,這時就需要搭建 docker 的私有倉庫,就好像 gitlab 一樣。

        上一篇 構建出鏡像后,我們可以部署一個私有鏡像倉庫用來存放我們的鏡像。

        啟動私有 Registry

        啟動一個私有倉庫也非常簡單,在服務器上執行命令

        docker run -d -p 5000:5000 --name="docker-registry" --restart=always -v /root/docker/registry/:/var/lib/registry/ registry 

        即后臺啟動 registry 鏡像構建出來的容器,并命名為 docker-registry,端口號映射為 50005000。

        --restart=always 代表當容器因為某些原因停止時,不管退出碼是什么都自動重啟。除了 always 還有 on-failure 代表只有退出碼不為 0 時才重啟,并且接受重啟次數參數:--restart=on-failture:5

        -v 指定將宿主機的 /root/docker/registry/ 目錄掛載到容器的 /var/lib/registry/ 目錄。這樣我們不用進入容器,在宿主機上就能訪問到容器內我們感興趣的目錄了。

        為什么是 /var/lib/registry/ 目錄?
        倉庫默認存放鏡像等信息在容器的 /var/lib/registry/docker 目錄下,可以進入該目錄查看已上傳鏡像信息。

        clipboard.png

        執行 run 命令成功后使用 docker ps 能看到 registry 服務已經啟動:

        clipboard.png

        上傳鏡像

        要上傳鏡像到私有倉庫,需要在鏡像的 tag 上加入倉庫地址:

        docker tag express-app 111.111.111.111:5000/sunhengzhe/express-app:v1

        為了不與其他鏡像沖突,可以加入命名空間如 sunhengzhe,另外最好給鏡像打上 tag 如 v1。

        注意倉庫地址沒有加協議部分,docker 默認的安全策略需要倉庫是支持 https 的,如果服務器只能使用 http 傳輸,那么直接上傳會失敗,需要在 docker 客戶端的配置文件中進行聲明。

        mac 配置

        clipboard.png

        clipboard.png

        更改完需要 Apply & Restart

        centos 系統

        /etc/docker/daemon.json 文件中寫入:

        {
          "registry-mirror": [
            "https://registry.docker-cn.com"
          ],
          "insecure-registries": [
            "[私有倉庫 ip:port]"
          ]
        }

        然后重啟 docker

        systemctl restart docker

        推送鏡像

        打完 tag 后使用 push 命令推送即可:

        docker push 111.111.111.111:5000/sunhengzhe/express-app:v1

        clipboard.png

        推送失敗

        如果出現 Retrying in 5 seconds 然后上傳失敗的問題??梢允紫仍诜掌魃鲜褂?logs 命令查看日志:

        docker logs -f docker-registry

        -f 代表持續輸出文件內容。

        如果出現 filesystem: mkdir /var/lib/registry/docker: permission denied,可能是 一個 selinux 問題,需要在服務器上對掛載目錄進行處理:

        chcon -Rt svirt_sandbox_file_t /root/docker/registry/

        此示例中即 /root/docker/registry/。

        clipboard.png

        拉取鏡像

        使用 pull 命令即可

        docker pull 111.111.111.111:5000/sunhengzhe/express-app:v1
        查看原文

        贊 1 收藏 0 評論 0

        認證與成就

        • 獲得 220 次點贊
        • 獲得 14 枚徽章 獲得 0 枚金徽章, 獲得 3 枚銀徽章, 獲得 11 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        (??? )
        暫時沒有

        注冊于 2014-09-04
        個人主頁被 2k 人瀏覽

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