吉利:到2020年新能源汽車銷量將實現90%以上的占比

日前,吉利控股集團董事長李書福在接受《中國經營報》記者採訪時表示,以後我們開發的新產品基本是新能源汽車和智慧互聯汽車,傳統汽車就逐漸不生產了。

此話背後,實際就是吉利於近日提出的“藍色吉利行動”中的重要一環,到2020年,其新能源汽車銷量將實現90%以上的銷量占比。

具體來說,根據“藍色吉利行動”,吉利新能源將在技術上主打純電動、油電混動、插電式混合動力三種路線,並通過兩大平臺——FE(中高端)與PE(緊湊級)平臺來發展純電動車板塊。

從以上路徑上看,吉利與其他車企並無二致,但其公佈的“五大承諾”卻足以讓業內熱議——第一,提前全面實現2020年國家第四階段每百公里5.0L的企業平均燃油消耗限值;第二,實現消費者用傳統汽車的購買成本購買插電式混動汽車的夢想;第三,實現到2020年新能源汽車銷量占吉利整體銷量的90%以上;第四,在氫燃料及金屬燃料電池汽車研發方面取得實質性成果;第五,實現新能源技術,智慧化、輕量化技術在行業的領先地位。

雖然言之鑿鑿,但不得不說的是,90%以上的新能源銷量加上此前吉利發佈的“2020年實現120萬輛”的銷量目標,同年吉利新能源汽車銷量目標竟逾100萬輛。

對此,吉利控股集團總裁CEO安聰慧表示:“吉利制定這樣的目標並不是為了和其他企業進行對比,而是結合自身發展提出來的。”

據其介紹,在技術領域,吉利汽車將以與沃爾沃合作打造的CMA中高級車基礎模組架構為核心打造新能源車型,該方面的設計研發工作主要由吉利汽車歐洲研發中心承擔,該中心在瑞典哥德堡已有1200名工程師,中國杭州也有300名工程師,負責架構開發、上車體開發、核心部件開發開發,整車設計、工程製造及新技術的研發。CMA基礎模組架構可以實現電機+發動機等核心部件的批量生產。

此外,2015年初,吉利與新大洋機電集團成立合資公司並推出知豆電動車,加之此前其子公司上海華普國潤與康迪車業成立的合資公司,署名吉利旗下的新能源車型並不止吉利品牌。雖然,吉利在新能源領域的開疆破土頗有“借力而為”之感,但不論如何,2015年1~11月,在乘聯會統計的自主品牌新能源車銷量占比情況中,吉利節節攀升,的確實現了在自主品牌領域的市占率穩增,也正因如此,加之政策的多重鼓勵,吉利才許下到2020年實現新能源汽車逾百萬的戰略目標。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

北汽豪擲80億元佈局新能源汽車

2015年12月28日,北京汽車集團有限公司就“北汽新能源汽車動力電池”、“北汽集團常州產業基地”兩個專案,與常州市政府簽約。

此次北汽集團擬在常州建設的兩個項目總投資80億元,其中,北汽新能源汽車動力電池專案總投資約30億元,規劃動力電池產能達到5G瓦時,同時將以滆湖低碳濕地公園培訓中心為主體,打造北汽新能源綠色商學院,其主要目的是加強新體系電池基礎研究和關鍵技術開發,推進新一代鋰離子電池的工程化和產業化,實現對動力電池產業鏈核心環節資源掌控,以支撐北汽新能源業務需求。

而北汽集團常州產業基地專案總投資50億元,總規劃年產30萬輛整車及配套零部件、物流專案,其中一期年產15萬輛SUV、MPV和輕型客車,二期重點生產新能源汽車,打造產業生態鏈。

此前,北汽集團總投資100億元的新能源汽車和總投資50億元的通用航空兩個項目已經於今年4月和10月相繼落戶常州。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

源碼學習系列之SpringBoot自動配置(篇二)

源碼學習系列之SpringBoot自動配置(篇二)之HttpEncodingAutoConfiguration 源碼分析

繼上一篇博客之後,本博客繼續跟一下SpringBoot的自動配置源碼

ok,先複習一下上一篇的內容,從前面的學習,我們知道了SpringBoot的自動配置主要是由一個選擇器AutoConfigurationImportSelector,先通過選擇器將自動配置的類加載到Spring容器

注意點:

  • List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);獲取的候選配置的類名
  • 由SpringFactoriesLoader加載器負責加載配置類名,已經裝載配置類到容器,SpringFactoriesLoader的loadSpringFactories方法讀取自動配置工程的META-INF/spring.factories配置文件,加載配置類的全類名,包裝成Properties對象,然後再加載到容器里
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    /* 將spring.factories的類都裝載到Spring容器*/
     public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();

        try {
        //將META-INF/spring.factories文件里配置的屬性都裝載到Enumeration數據結構里
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            ArrayList result = new ArrayList();
            //遍歷獲取屬性,然後再獲取對應的配置類全類名
            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
                String factoryClassNames = properties.getProperty(factoryClassName);
                result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            }

            return result;
        } catch (IOException var8) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
        }
    }

ok,Springboot的自動配置類都在這個包里,源碼很多,所以本博客只是簡單跟一下源碼

自動配置可以說是SpringBoot框架的一個很重要的功能,其強大的功能就是通過很多配置類實現的,當然這麼多配置,可以參考SpringBoot官方的配置參考:
https://docs.spring.io/spring-boot/docs/2.1.10.RELEASE/reference/html/common-application-properties.html

SpringBoot的自動配置類很多,顯然不是每個配置類都生效的,比如你沒引對應的jar,那對應的配置類肯定是不起效的,ok,本博客以HttpEncodingAutoConfiguration自動編碼配置類為實例,記錄一下SpringBoot的自動配置

先補充一些@Conditional註解的用法:詳情可以參考我上篇博客

@Conditional派生註解 作用(都是判斷是否符合指定的條件)
@ConditionalOnJava 系統的java版本是否符合要求
@ConditionalOnBean 有指定的Bean類
@ConditionalOnMissingBean 沒有指定的bean類
@ConditionalOnExpression 符合指定的SpEL表達式
@ConditionalOnClass 有指定的類
@ConditionalOnMissingClass 沒有指定的類
@ConditionalOnSingleCandidate 容器只有一個指定的bean,或者這個bean是首選bean
@ConditionalOnProperty 指定的property屬性有指定的值
@ConditionalOnResource 路徑下存在指定的資源
@ConditionalOnWebApplication 系統環境是web環境
@ConditionalOnNotWebApplication 系統環境不是web環境
@ConditionalOnjndi JNDI存在指定的項

通過上篇博客的學習,我們已經知道了SpringBoot有很多自動配置類,所以本博客拿HttpEncodingAutoConfiguration類來看看

補充:

  • @Configuration proxyBeanMethods屬性:默認是開啟的,開啟后允許其它配置類調用這個類的@bean方法,詳情參看Spring官方文檔:
package org.springframework.boot.autoconfigure.web.servlet;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.http.HttpProperties;
import org.springframework.boot.autoconfigure.http.HttpProperties.Encoding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;

@Configuration(
    proxyBeanMethods = false
)//指定是一個配置列,關了proxyBeanMethods,其它配置類就不能調相應的@bean類
@EnableConfigurationProperties({HttpProperties.class})//讓使用 @ConfigurationProperties註解的HttpProperties類生效,HttpProperties類通過ConfigurationProperties註解,將屬性配置一個一個加載進來
@ConditionalOnWebApplication(
    type = Type.SERVLET
)//指定系統環境是Web環境配置才起效,並且指定類型是SERVLET
@ConditionalOnClass({CharacterEncodingFilter.class})//系統有CharacterEncodingFilter過濾器類,則配置類起效,CharacterEncodingFilter類:SpringMVC中進行亂碼解決的過濾器
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)//判斷配置文件是否有spring.http.encoding.enabled屬性,如果沒配置,也是默認為true的,因為配置了`matchIfMissing =true`
public class HttpEncodingAutoConfiguration {
    //創建一個Encoding對象
    private final Encoding properties;
    // 構造函數里通過properties.getEncoding();進行屬性映射,獲取默認的配置
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }

    @Bean
    @ConditionalOnMissingBean//如果系統沒有CharacterEncodingFilter類,就執行characterEncodingFilter方法
    public CharacterEncodingFilter characterEncodingFilter() {
        //重新創建一個編碼過濾器
        OrderedCharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        //設置默認的配置
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }

    @Bean
    public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
        return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
    }

    private static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
        private final Encoding properties;

        LocaleCharsetMappingsCustomizer(Encoding properties) {
            this.properties = properties;
        }

        public void customize(ConfigurableServletWebServerFactory factory) {
            if(this.properties.getMapping() != null) {
                factory.setLocaleCharsetMappings(this.properties.getMapping());
            }

        }

        public int getOrder() {
            return 0;
        }
    }
}

通過對HttpEncodingAutoConfiguration源碼的學習,可以看出,其實主要是用@Conditional及其派生註解,這些註解都是要在特定情況才會起效,起效了,才會將組件加載到Spring容器里

ok,然後我們怎麼知道哪些配置是起效的?在SpringBoot項目里,是可以通過配置,開啟打印的,可以在application.properties加上debug=true屬性就可以

控制台打印的Positive matches就表示有效的配置類

console打印的Negative matches表示不起效的配置類:
比如我的項目沒有加aop的,aop自動配置類就不起效

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

代碼注入及其拓展–逆向開發

今天繼續講述逆向開發中另一個比較重要的課程是代碼注入內容,本篇篇幅比較長,但還是有很多乾貨的,希望大家通過此篇文章更加了解逆向開發中的要點和知識點.我們將分解幾個內容,進行講解:

  1. Framework注入
  2. Dylib注入
  3. MethodSwizzle
  4. 微信示例講解
  5. 總結

讓代碼執行自己的代碼,整體方案如下:

如何讓別人的app來執行自己的代碼呢? 這就要通過代碼注入的方式來達到,而代碼注入的方式有兩種: 一種是通過framework, 一種是dylib方式,另種方案,可以通過Runtime機制

代碼注入思路:

DYLD會動態加載動態庫Framework中所有動態庫,在frameworks中加入自己的一個動態庫,然後在動態庫中hook和注入代碼.

一、FrameWork注入

 1.準備工作

  • 微信6.6.5(越獄應用)
  • MachOView軟件

  MachOView的下載地址:

  如果想看源碼如下:MachOView源碼:

  • yololib工具(給MachOView注入framework)

  yololib工具下載地址:

  • 簽名文件appsign文件

2.流程

2.1 加入準備工作,導入微信6.6.5版本以及腳本appSign.sh重簽名文件

2.2 將appSign導入到項目腳本中

 

 

 2.3 有了上面的兩個步驟后,然後編譯一下工程,會出現一個temp工程,裡面包含了payload文件

2.4 显示包內容,查看可執行文件

 2.5 我們通過MachOView軟件查看WeChat

我們看到有很多的DYLIB,代表的是加載動態庫

2.6  我們在項目中新建framework

 

2.7 新建文件用於驗證

2.8 想要達到剛加載就運行,代碼要寫在load方法

 2.9 編譯一下,查看app包位置會多出一個framework

2.10 显示包內容,在framework查看

由上可知,WJHookFrameWork已經加入成功。

2.11 但是運行並沒有成功,沒有執行load里的代碼

原因:用MachOView打開可執行的WeChat,沒有找到WJHookFrameWork

下面我們講述怎麼將WJHookFramework寫入到MachoView文件中?

3. WJHookFramework寫入到MachOView文件中

需要使用yololib工具,建議將yololib放到 /usr/local/bin

3.1 解壓越獄包

3.2 將WeChat.app显示包內容,找到WeChat可執行的文件

需要增加執行權限: chmod +x WeChat

3.3 寫入WeChat可執行文件

yololib WeChat Frameworks/WJHookFrameWork.framework/WJHookFrameWork

通過上面的過程,查看MachOView文件Load commands中是否有WJHookFrameWork

上面圖显示已經加入成功。

3.4 刪除原有的ipa,打包payload

zip -ry WeChat.ipa Payload

將WeChat.ipa放入App目錄中,刪除其他的文件夾。

 

3.5 再次運行,發現成功!!!

上面就是framework方式代碼注入。大家可以私信我,如有不懂!!!

 二、Dylib注入

2.1 新建工程,添加腳本到build phases 


加入腳本文件

2.2添加第三方庫dylib(mac os的,非ios)

2.3 添加依賴

2.4 修改第三方類庫僅限mac使用,修改Base SDK

2.5 修改signing 

2.6 腳本中注入動態庫的代碼

# ${SRCROOT} 它是工程文件所在的目錄
TEMP_PATH="${SRCROOT}/Temp"
#資源文件夾,我們提前在工程目錄下新建一個APP文件夾,裏面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
#目標ipa包路徑
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夾
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"

#----------------------------------------
# 1. 解壓IPA到Temp下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解壓的臨時的APP的路徑
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路徑是:$TEMP_APP_PATH"

#----------------------------------------
# 2. 將解壓出來的.app拷貝進入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路徑
# TARGET_NAME target名稱
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路徑:$TARGET_APP_PATH"

rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

#----------------------------------------
# 3. 刪除extension和WatchAPP.個人證書沒法簽名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"

#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
#  設置:"Set : KEY Value" "目標文件路徑"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

#----------------------------------------
# 5. 給MachO文件上執行權限
# 拿到MachO文件的路徑
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可執行權限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"

#----------------------------------------
# 6. 重簽名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do

#簽名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi

#注入
yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libHankHook.dylib"

2.7 編譯運行成功(也和上面一樣在類中加入load代碼)

 

上面就是dylib方式代碼注入,希望對大家有所幫助!!!

 通過上面的兩種方式實現代碼注入,讓別人的app運行自己的app,下面總結如下:

 三、MethodSwizzle

3.1 概念

iOS 中實現AOP編程思想的方式其中之一是Method Swizzling,而 Method Swizzling 是利用Runtime特性把一個方法和另個方法的實現做替換,程序運行時修改Dispatch Table里SEL和IMP之間的映射關係.

通過swizzling method改變目標函數selector所指向實現,在新的實現中來實現所要改的內容即可.

3.2 特點

  • 繼承: 修改較多,無法敢保證他人一定繼承基類
  • 類別: 類別中重寫方法會覆蓋到原有的實現,其實,在真實的開發中,重寫方法並不是為了取代它,而是為了添加一些實現; 如果幾個類別實現了同樣的方法, 但只有一個類別的方法會被調用.
  • AOP優勢: 減少了重複代碼

3.3 代碼

@implementation NSURL (HKURL)

+(void)load
{
    Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
    
    Method HKURL = class_getClassMethod(self, @selector(HKURLWithStr:));
    
    //交換
    method_exchangeImplementations(URLWithStr, HKURL);
}

+(instancetype)HKURLWithStr:(NSString *)str{
    //調用系統原來的方法
    NSURL * url = [NSURL HKURLWithStr:str];
    if (url == nil) {
        str = @"https://www.blog.com";
    }
    url = [NSURL HKURLWithStr:str];
    
    return url;
}

在上面的代碼中,利用method swizzling的交換方法.其他Runtime的使用方法,以及為什麼寫在load方法中,請參考本人另篇博客

拓展: 為什麼寫在load中?

  • load方法在源文件被裝載到程序中會被自動調用,不需要手動調用,也不需要該類使用不使用無關,在main()前被執行.
  • 當子類重寫了load,假如子類的類別重寫了load,load的調用順序會這樣: 父類、子類、子類類別
  • 但是如果有多個子類category都重寫了load,每個子類category中load都會調用一次
  • 假如子類沒有重寫load,子類的默認load也不會去調用父類的load.此與正常繼承不太一樣.
  • 在正常的開發中, 除了method swizzle ,其他的邏輯代碼盡量不放在load,load方法中的代碼邏輯要盡量簡單

 

四、微信示例Demo

4.1 微信–破壞註冊

4.1.1 將微信程序運行出來,如下圖所示

4.1.2 根據上面紅色找出類名,方法名

4.1.3 通過插件class-dump導出微信的.h文件

class-dump是將OC運行時聲明的信息導出來的工具, 其實可以導出.h文件. 用此工具將未經過加密的app的頭文件導出來.

使用它同樣也要講此工具拷貝到MAC的目錄下/usr/local/bin下.

4.1.4 經過sublime text來找出對應的文件

4.1.5 通過第三部分講解,利用runtime交換方法

4.1.6 結果

 

4.2 竊取微信密碼

4.2.1 找到輸入密碼框的內容

從上面看出,登錄按鈕為一個FixTitleColorButton對象,Target名字存放的地址為0x280afaa40,Action名字存放地址是0x280afac00。

4.2.2 查看賬號密碼的輸入框

發現賬號密碼輸入框對象屬於都一個對象,叫做WCUITextField

4.2.3 利用LLDB查看登錄具體的Target和Action

從上面卡出,登錄按鈕在WCAccountMainLoginViewController頁面中;

登錄點擊方法叫做onNext

4.2.4 利用Sublime查看WeChat文件

發現確實有onNext()方法,並從中看出賬號輸入框和密碼輸入框都是WCAccountTextFieldItem中,但是並沒有發現textFileld,但是可以看到WCAccountTextFieldItem是繼承於WCBaseTextFieldItem,我們再看看WCBaseTextFieldItem文件內容

看出一個m_textField對象,通過tex字段取出string。

4.2.5 在賬號欄中輸入賬號和密碼

po [(WCAccountMainLoginViewController *)0x1128bbc00 valueForKey:@"_textFieldUserPwdItem"]
po [(WCAccountTextFieldItem *)0x28328e880 valueForKey:@"m_textField"]
po [(WCUITextField *)0x112163a00 text]

通過LLDB調試輸入的密碼是123456。

4.2.6 Hook登錄,獲取密碼

+ (void)load {
    NSLog(@"來了,老弟");
    Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), sel_registerName("onNext"));
    //1.保存原始的IMP
    old_onNext = method_getImplementation(onNext);
    //2.SET
    method_setImplementation(onNext, (IMP)my_next);
}

IMP (*old_onNext)(id self,SEL _cmd);

void my_next(id self,SEL _cmd){
    // 獲取密碼
    NSString *pwd = [[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"] performSelector:@selector(text)];
    NSString *accountTF = [[[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"] performSelector:@selector(text)];
    NSLog(@"密碼是!%@",pwd);
    // 將密碼追加在賬號欄的後面
    [[[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"] performSelector:@selector(setText:) withObject:[NSString stringWithFormat:@"%@+%@",accountTF,pwd]];
    //調用原來的方法
    old_onNext(self,_cmd);
}

上面用的是setIMP和getIMP的方式,對原方法進行Hook,也可以用class_replaceMethod(),method_exchangeImplementations()。

 

五、總結

首先從代碼注入的方式:framework和dylib兩種方式,然後講到Method swizzling方式嘗試Hook,最後又以demo的方式來闡述代碼注入和Hook,希望對大家理解逆向開發的代碼注入有所幫助!!!,歡迎大家繼續關注!!!

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

對於計算機相關專業我們在迷茫什麼

計算機相關專業初識–對於計算機相關專業我們在迷茫什麼

前言

由於種種原因,迫使我寫下這篇博客,我相信,初入計算機相關專業的萌新肯定很迷茫,我該學什麼,我該如何去學,我該如何學好等等問題纏繞心頭。有很多學弟學妹問我該如何去學計算機相關專業,作為過來人,我決定將我的所知所得寫下來,讓初入計算機相關專業的萌新的學習之路走得更順暢一些。

一、什麼是計算機

對於剛學習計算機相關專業的萌新來說,了解一下計算機的工作原理是十分必要的,但是在這裏我們不過多闡述,讓大家簡單了解一下就好。

讓我們先來看一下對於計算機名詞的解釋:

計算機(computer)俗稱電腦,是現代一種用於高速計算的电子計算機器,可以進行數值計算,又可以進行邏輯計算,還具有存儲記憶功能。是能夠按照程序運行,自動、高速處理海量數據的現代化智能电子設備。

划重點:

  • 我們注意到,計算機就是一種用於進行數值計算的現代化智能电子設備。需要理解的是為什麼是進行數值計算,在這裏,你會疑惑,為什麼是數值計算呢,我輸入的明明不是数字呀?這個問題很容易解釋清楚,因為計算機只是一種电子設備,它不具有人類獨立思考和不斷學習的能力,它的所有功能都是事先設定好的,所以當計算機面對輸入字符的時候,會將它統一按照ASCII(計算機編碼系統)規則轉換為數值“0”和“1”(二進制數值),所以,在計算機里,數據存儲都是用“0”和“1”(即二進制數值)來實現。

  • 還有一點值得注意,按照程序運行,那麼問題來了,程序是什麼?程序就是一組計算機能識別和執行的指令, 它以某些程序設計語言編寫,運行於某種目標結構體繫上 。舉個例子,程序就像是用英語(程序設計語言,例如c,c++)寫的文章,要讓一個懂的英語的人(編譯器,如C的編譯器gcc,這裏要注意編譯器和IDE的區別,通常IDE包含編譯器)同時也會閱讀這篇文章的人(結構體系)來閱讀、理解、標記這篇文章。

有學妹問過我,為什麼簡單的代碼,能實現豐富的效果。其實這取決於編譯器的強大能力。下面來簡單介紹一下,編輯器,編譯器,IDE(集成開發環境)的區別。

  • 編輯器:編輯器就是用來編輯的軟件,比如windows自帶的記事本就是一個編輯器, 記事本沒有語法高亮,不显示行號,當一段可執行代碼寫完后無法通過內置環境執行,必須手動輸入命令執行編譯等等一些弊端,所以很少有程序員會用記事本去寫代碼 , 寫代碼比較好用的編輯器軟件有vscode,vim,sublime,notepad++,emacs,atom等等 ,雖然編輯器原始功能不足,但是開發人員為了使編輯器更加友好,所以有很多內置插件可供使用,完全可以手動打造一個IDE。
  • 編譯器:簡單來說,編譯器就是將“一種語言(一般為高級語言,如c,c++,java,python等,計算機不可直接識別和執行)”翻譯為“另一種語言(一般為低級語言,低級語言即機器語言,機器語言是用二進制代碼錶示的計算機能直接識別和執行的一種機器指令的集合)”的程序。舉個例子,用Dev-C++寫好一段可執行"hello,world!"C語言代碼之後,我們要讓它在屏幕打印出來我們想要它輸出的"hello,world!",就需要通過gcc編譯器執行編譯后才能显示。其他語言同理。
  • IDE:集成開發環境,用於程序開發環境的應用程序,一般包含代碼編輯器編譯器調試器圖像用戶界面等工具。集成了代碼編寫程序分析程序編譯程序調試等功能。如 jetbrains 的用於Java開發的 IntelliJ IDEA 、用於JavaScript開發的WebStorm、用於Python開發Pycharm,微軟的 Visual Studio系列 ,IBM的Eclipse。

二、我們該學什麼

很多初入計算機相關專業的萌新,總是很迷茫,不知道自己該學什麼,通常是他們知道如何去學好學校開設的每一門課程,就是不知道自己該向哪些方向學習,這些方向指的是專業技能和就業方向,諸如web開發、Android/IOS開發、數據分析、人工智能、網絡安全、遊戲開發、軟件測試等等。有這種疑惑很正常,迷茫也是正常的,但我們總要讓自己了解自己所需,然後腳踏實地,一步一步去充實自己的能力。而我想做的也很簡單,就是幫助大家解除心裏的疑惑。那麼,我們開始進入正題。

1. 我們該如何選擇適合自己的方向

對於這個問題,其實是很難回答清楚的,因為每個人的興趣都不相同,所以就很難去站在自己的角度去回答疑問者的問題。但是,原理都是想通的,我相信我的經驗會幫助到你們。

  • 通常,學校每學期都會開設一門或多門語言(程序設計語言,下文同),那麼,喜歡一門語言,首先要愛上它的語言風格,諸如Java的嚴謹,Python的自由,總有一款適合你;其次,在學習語言的過程中,一定要了解它能幹什麼,市場環境如何,工作崗位多少等綜合因素,再決定要不要去深入這門語言,並且主攻自己感興趣的那個方向。

  • 對於學校沒有開設,但是自己又想學習的語言而言,該如何去選擇。首先,學校開設的語言基本是市場比較流行的語言,也符合市場需求,所以,完全可以在學校開設的語言中去選擇自己想要了解並學習的語言。此外,我們可以藉助 TIOBE ( TIOBE 編程社區指數是編程語言流行度的指標,該榜單每月更新一次,指數基於全球技術工程師、課程和第三方供應商的數量。包括流行的搜索引擎,如谷歌、必應、雅虎、維基百科、亞馬遜、YouTube 和百度都用於指數計算。 )去了解語言的流行程度,流行程度決定市場需求,以此來參考自己想要了解並學習的語言,在此附上2019年11月語言排名。

2. 主流編程語言主要應用場景

  • Java

    1. 企業級應用開發: 大到全國聯網的系統,小到中小企業的應用解決方案,Java都佔有極為重要的地位 。
    2. web後端開發: JSP+Servlet+JavaBean 是一種比較流行的開發模式。
    3. 移動領域:手機遊戲。
    4. Android App開發: android 開發只用到了JAVA的語法和JAVA SE的一小部分API。
  • C

    C語言是一門基礎語言,是其他一些語言的基礎,例如MATLAB,Object-C,Lua等.同時也是學習來比較難的語言,達到精通的程度沒有3-10年左右很難,C語言沒有比較完善的開發框架,是面向過程的一門語言,講究算法跟邏輯。

    1. 科研
    2. 服務器: 網絡核心設備,如路由器、交換機、防火牆。
    3. 操作系統:類unix系統(Linux/freebsd)
    4. 嵌入式開發: 在一個特定的硬件環境上開發與構建特定的可編程軟件系統的綜合技術。
    5. 自動化控制
  • Python

    1. 圖形處理
    2. 數學處理
    3. 文本處理
    4. 數據庫編程
    5. 網絡編程
    6. 多媒體應用
    7. pymo引擎: 運行於Symbian S60V3,Symbian S60V5,Symbian 3,Android,Windows,Linux,Mac Os,Maemo,MeeGo系統上的AVG遊戲引擎。
    8. 黑客編程
    9. 網絡安全
  • C++

    1. 遊戲開發
    2. 科學計算
    3. 網絡軟件
    4. 操作系統
    5. 設備驅動程序
    6. 移動設備
    7. 嵌入式開發
    8. 科研
    9. 編譯器
  • C#

    1. web後端開發
    2. 桌面軟件開發
    3. 人工智能
    4. 遊戲開發
  • JavaScript
    唯一能用於前後端開發的語言web前端開發
    1. web前端開發
    2. node web後端開發
    3. 操作系統
    4. 後台
    5. 桌面軟件開發
    6. 混合App
    7. 小程序
  • PHP

    1. web後端開發
    2. 桌面軟件開發
    3. 命令行腳本
  • SQL

    1. 操作數據庫
  • Swift

    1. 蘋果生態系統應用開發
  • Ruby

    1. web開發
  • R

    數據科學闖天下,左手Python右手R

    1. 機器學習
    2. 數據分析
    3. 科學計算
  • Go

    1. web後端開發
    2. 高性能服務器應用

3. 主流編程語言學習路徑(將持續更新,僅供參考)

  • JavaScript

4. 主流編程語言入門學習書籍推薦

語言 書籍
C 《嗨翻C語言》
C++ 《C++權威教程》
Java 《Java輕鬆學》
Python 《Python編程從入門到實戰》
JavaScript 《JavaScript入門經典》
PHP 《PHP編程實戰》
SQL 《SQL基礎教程》
Swift 《Swift編程權威指南》
Ruby 《Ruby從入門到精通》
R 《R語言實戰》
Go 《Go語言聖經》

5. 編程學習網站推薦

網站 網址
菜鳥教程
W3School
實驗樓
猿學
慕課網
SegmentFault
博客園
GitHub
掘金
學習數據科學
易百教程
看雲

三、總結

通篇寫完,感覺自己也重新學到了很多,學習就是一個反覆複習的過程,每次學習都能帶給自己不一樣的收穫。希望以上內容可以給初入計算機相關專業的萌新帶來一些幫助,後面我會不斷更新和優化本文,請大家持續關注。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

ES6學習筆記01 — 暫時性死區 ( temporal dead zone )

參考文檔:   

       

       

注:文中代碼僅作示意,複製運行時需要適當調整

  
ES6 規定,如果代碼區塊中存在 let  const 命令聲明的變量,這個區塊對這些變量從一開始就形成了封閉作用域,直到聲明語句完成,這些變量才能被訪問(獲取或設置),否則會報錯ReferenceError。這在語法上稱為“暫時性死區”(英temporal dead zone,簡 TDZ),即代碼塊開始到變量聲明語句完成之間的區域。
  通過 var 聲明的變量擁有變量提升、沒有暫時性死區,作用於函數作用域:

    • 當進入變量的作用域(包圍它的函數),立即為它創建(綁定)存儲空間,立即被初始化並被賦值為 undefined   
    • 當執行到變量的聲明語句時,如果變量定義了值則會被賦值
    (function fn() {  //函數作用域開始
        console.log(temp)  //undefined
        //聲明
        var temp 
        console.log(temp)  //undefined
        //賦值
        temp = 123
        console.log(temp)  //123
    })()
    //在函數作用域外訪問
    console.log(temp)  //ReferenceError: temp is not defined

 

  通過 let 聲明的變量沒有變量提升、擁有暫時性死區,作用於塊級作用域:

    • 當進入變量的作用域(包圍它的語法塊),立即為它創建(綁定)存儲空間,不會立即初始化,也不會被賦值
    • 訪問(獲取或設置)該變量會拋出異常 ReferenceError
    • 當執行到變量的聲明語句時,如果變量定義了值則會被賦值,如果變量沒有定義值,則被賦值為undefined
        {  //函數作用域開始,TDZ開始
            console.log(temp)  //ReferenceError: temp is not defined
            //聲明
            let temp  
            console.log(temp)  //ReferenceError: Cannot access 'temp' before initialization
            //賦值
            temp = 345  //TDZ結束
            console.log(temp)  //345
            //塊級作用域結束
        }
        //在塊級作用域外訪問
        console.log(temp)  //ReferenceError: temp is not defined

 

  通過 const 聲明的常量,需要在定義的時候就賦值,並且之後不能改變,暫時性死區與 let 類似。

        {   //作用域開始,TDZ開始
            console.log(temp)  //ReferenceError: temp is not defined
            //聲明並賦值
            const temp = 789  //TDZ結束
            console.log(temp)  //789 
            //給常量賦值
            temp = 987  //TypeError: Assignment to constant variable
            //作用域結束
        }   
        //在作用域外訪問
        console.log(temp)  //ReferenceError: temp is not defined

 

  
一句話總結:在塊級作用域中, let  const 聲明的變量、常量在聲明語句執行完成之前不能訪問(包括聲明語句本身)
  另外,容易因為 暫時性死區 而出錯的細節代碼在此不展開舉例,參考文檔中有詳情舉例。  
  附:第一次寫學習筆記,寫隨筆花費的時間比學習相關內容的時間還要長,感覺有點本末倒置,不過以後回頭看應該會覺得有所值得吧!  

  

  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

電動摺疊腳踏車、滑板車搶攻最後一哩交通需求

傳統交通工具汽機車造成許多空氣污染與碳排放,許多廠商認為解決方案是電動車、電動機車,不過,也有人提出另一個想法,那就是何不更充分利用大眾運輸工具,只要解決捷運站到目的地的「最後一哩」交通需求即可鼓勵許多人不用自己開車、騎機車,而願意搭乘捷運。在 CES 2016 上,許多廠商都推出類似概念的產品。

Cycle Board 推出「街道衝浪者」(Street Surfer)電動滑板三輪車,這輛滑板車的前端有兩輪,連接把手,負責轉向,較大的雙前輪也提供在人行道等不平坦路面上的穩定性,後端有一個小輪,把手可伸縮配合使用者身高,也可收疊起來方便攜帶,把手上除了剎車等基本控制,還可裝上手機。「街道衝浪者」充電一次可行駛 15 到 20 英里(24.1 到 32.2 公里),最高時速 20 英里(32.2 公里)。

▲ Street Surfer(Source:) Urban626 則推出 Urb-e 折疊式電動腳踏車,結構像是一把折疊刀,設計師宣稱可快速折疊,實測大約 1 秒鐘可折疊起來,便於帶電車車廂,或是裝進汽車後車廂,骨架以鋁打造,總重 35 英磅(15.876 公斤),時速最高 24 公里,充電 4 小時充飽後,可以行駛 32 公里。   由 Smart Rhino 推出的 Xcooter 同樣是電動折疊式腳踏車,以 X 形狀骨架收合,充電 3 小時可行駛 28 公里,最高時速 80 公里,重量 18 公斤。這些可折疊的電動腳踏車或許是解決捷運站到目的地交通問題的解決方案,不過售價可不便宜,Urb-e 售價 1,500 美元,將近 5 萬元新台幣,Xcooter 預售價 1,499 美元,而 2016 年 4 月正式上市時售價將為 1,799 美元,將近 6 萬元新台幣。

(本文授權轉載自《》─〈〉)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

2016上海國際汽車新能源及智慧技術展覽會

創新技術 智能演繹 展會日期:2016年6月28日-30日 展會地點:上海新國際博覽中心   觀眾數量:30,000人次(預計) 展覽週期:二年一屆,2016年首屆 同期展會:上海國際汽車零部件及相關服務展覽會   主辦單位: 中國國際貿易促進委員會上海市分會 中國國際貿易促進委員會汽車行業分會 中國汽車工程學會 上海市國際展覽有限公司   展位價格: 室內光地:650元/平方米(36平米起租) 標準展位:900元/平方米(9平米起租)   參展範圍: -節能汽車、純電動車,混合動力車,燃料電池車,輕型電動車,天然氣(液化氣)車,醇類及其他代用燃料車和節能汽車。 -電池、電機、電控等核心零部件和先進技術應用;輕量化材料、整車優化設計及混合動力等節能技術產品; 自動駕駛,汽車共用,模組化(集成化)構造,汽車車載資訊系統及智慧化設備;汽車相關後市場服務及產品。 -充電樁、充電機、配電櫃、充換電池及電池管理系統、停車場充電設施、智慧監控、充電站供電解決方案、充電站-智慧電網解決方案等。 -檢測,維修,監控,實驗,安全防護裝備及媒體等.   展會諮詢: 北京盛大超越國際展覽有限公司 連絡人:岳巍 先生 電話(微信):135 5286 5285 郵箱:     附:其他推薦展會   2016中國國際汽車新能源及技術應用展覽會 節能與新能源汽車產業發展規劃成果展覽會   展會時間:2016年10月13-16日 展會地點:北京•國家會議中心 展覽規模:30,000平方米(2015年) 觀眾數量:60,000人次(2015年) 展覽週期:每年一屆,2013年首屆 詳情請點擊  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

Ansible之playbook拓展

  一、handlers和notify結合使用觸發條件

  handlers同tasks是屬同級,相當於一個特殊任務列表,這些任務同前文說的tasks里的任務沒有本質的不同,用於當關注的資源發生變化時,才會採取一定的操作。notify此action可用於在每一個play的最後被觸發,這樣可避免多次有改變發生時都執行指定的操作,僅在所有的變化發生完成后一次性地執行指定操作,在notify中列出的操作稱為handler,換句話說當所關注的資源發生變化時notify將調用handlers中定義的操作。其中notify所在任務就是被監控的任務資源變化的任務,notify可以調用多個handlers定義的操作,一個handlers里可以定義很多任務。

---
- hosts: websers
  remote_user: root

  tasks:
    - name: create apache group
      group: name=apache gid=80 system=yes
    - name: create apache user
      user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
    - name: install httpd
      yum: name=httpd
    - name: copy config file
      copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
      notify: restart httpd service

    - name: start httpd service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: restart httpd service
      service: name=httpd state=restarted   

  說明:notify后指定的名稱必須要和handlers里的任務名稱相同,如不同handlers所定義的任務將不會執行,相當於沒有notify調用handlers里的任務。

  在某些情況下,我們可能同時需要調用多個handlers,或者需要使用handlers其他handlers,ansible可以很簡單的實現這些功能,如下所示

  1)調用多個handlers

---
- hosts: websers
  remote_user: root

  tasks:
    - name: create apache group
      group: name=apache gid=80 system=yes
    - name: create apache user
      user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
    - name: install httpd
      yum: name=httpd
    - name: copy config file
      copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
      notify: 
        - restart httpd service
        - check httpd process

    - name: start httpd service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: restart httpd service
      service: name=httpd state=restarted
    - name: check httpd process                                                                                      
      shell: /usr/bin/killall -0 httpd &> /tmp/httpd.log

  說明:調用多個handlers我們需要在notify中寫成列表的形式,同樣我們被觸發的任務名稱需要同handlers里的被調用的任務名稱完全相同

  2)handlers調用handlers

---
- hosts: websers
  remote_user: root

  tasks:
    - name: create apache group
      group: name=apache gid=80 system=yes
    - name: create apache user
      user: name=apache uid=80 group=apache system=yes shell=/sbin/nologin home=/var/www/html 
    - name: install httpd
      yum: name=httpd
    - name: copy config file
      copy: src=/tmp/httpd.conf dest=/etc/httpd/conf/
      notify: restart httpd service

    - name: start httpd service
      service: name=httpd state=started enabled=yes

  handlers:
    - name: restart httpd service
      service: name=httpd state=restarted
      notify: check httpd process                                                                                    
    - name: check httpd process
      shell: /usr/bin/killall -0 httpd &> /tmp/httpd.log

  說明:handlers調用handlers,則直接在handlers中使用notify選項就可以。

在使用handlers我們需要注意一下幾點:

  1)handlers只有在其所在任務被執行時才會被運行,handlers定義的任務它不會像task任務那樣,自動會從上至下依次執行,它只會被notify所在的任務發生狀態改變時才會觸發handlers 的任務執行,如果一個任務中定義了notify調用handlers,但由於條件的判斷等原因,該任務尚未執行,那麼notify調用的handlers同樣也不會執行。

  2)handlers只會在play的末尾運行一次;如果想要在一個playbook的中間運行handlers,則需要使用meta模塊來實現,如:-mate: flush_handlers

  二、playbook中變量的使用

ansible中變量的命名規範同其他語言或系統中變量命名規則非常類似。變量名以英文大小寫字母開頭,中間可以包含下劃線和数字,ansible變量的來源有很多,具體有以下幾點:

  1)ansible setup模塊,這個模塊可以從遠程主機上獲取很多遠程主機的基本信息,它所返回的所有變量都可以直接調用,有關setup說明請參考本人博客

  2)在/etc/ansible/hosts中定義,此文件是ansible執行名時默認加載的主機清單文件,在裏面除了可定義我們要管理的主機外,我們還可以定義針對單個主機定義單獨的變量,我們把針對單獨某一台主機定義的變量叫做普通變量(也可叫做主機變量);還有一種變量它不是針對單獨一個主機,它針對某一個組裡的所有主機,我們把這種變量叫做公共組變量。主機清單中定義的變量優先級是普通變量高於公共變量。

    2.1)主機變量,可以在主機清單中定義主機時為其添加主機變量以便於在playbook中使用,如下所示

[websers]
192.168.0.128 http_port=80 maxRequestsPerChild=808
192.168.0.218 http_port=81 maxRequestsPerChild=909

    2.2)主機組變量,組變量是指定賦予給指定組內所有主機上的在playbook中可使用的變量,如下所示

[websers]
192.168.0.128 http_port=80 
192.168.0.218 http_port=81 
[websers:vars]
maxRequestsPerChild=909

  3)通過命令行指定變量(-e指定變量賦值,可以說多個但需要用引號引起或者一個變量用一個-e指定賦值),這種在命令行指定的優先級最高。如下所示

ansible-playbook -e 'package_name1=httpd package_name2=nginx' test_vars.yml

  4)在playbook中定義變量,最常見的定義變量的方法是使用vars代碼塊,如下所示

---
- hosts: websers
  remote_user: root
  vars:
    - abc: xxx 
    - bcd: aaa  

  5)在獨立的變量yml文件中定義,在playbook中使用vars_files代碼塊引用其變量文件,如下所示

[root@test ~]#cat vars.yml 
---
package_name1: vsftpd
package_name2: nginx
[root@test ~]#cat test_vars.yml 
---
- hosts: websers
  remote_user: root
  vars_files:
    - vars.yml
  tasks:
    - name: install package1
      yum: name={{ package_name1 }}
    - name: install package2
      yum: name={{ package_name2 }}
[root@test ~]#

  6)在role中定義,這個後續說到角色在做解釋

  變量的調用方式:第一種在playbook中使用變量需要用“{{}}”將變量括起來,表示括號里的內容是一個變量,有時用“{{  variable_name }}”才生效;第二種是ansible-playbook -e 選項指定其變量,ansible-playbook -e “hosts=www user=xxxx” test.yml

  在主機清單中定義變量的方法雖然簡單直觀,但是當所需要定義的變量有很多時,並且被多台主機使用時,這種方法顯得非常麻煩,事實上ansible的官方手冊中也不建議我們把變量直接定義到hosts文件中;在執行ansible命令時,ansible會默認會從/etc/ansible/host_vars/和/etc/ansible/group_vars/兩個目錄下讀取變量定義文件,如果/etc/ansible/下沒有以上這兩個目錄,我們可以手動創建,並且可以在這兩個目錄下創建與hosts文件中的主機名或主機組同名的文件來定義變量。比如我們要給192.168.0.218 這個主機定義個變量文件,我們可以在/etc/ansible/host_vars/目錄下創建一個192.168.0.218的空白文件,然後在文件中以ymal語法來定義所需變量即可。如下所示

[root@test ~]#tail -6 /etc/ansible/hosts 
## db-[99:101]-node.example.com
[websers]
192.168.0.128 
192.168.0.218 
[appsers]
192.168.0.217
[root@test ~]#cat /etc/ansible/host_vars/192.168.0.218 
---
file1: abc
file2: bcd
[root@test ~]#cat test.yml 
---
- hosts: 192.168.0.218
  remote_user: root
  
  tasks:
    - name: touch file1
      file: name={{ file1 }} state=touch
    - name: toch file2
      file: name={{ file2 }} state=touch
[root@test ~]#ansible-playbook test.yml 

PLAY [192.168.0.218] ************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************
ok: [192.168.0.218]

TASK [touch file1] **************************************************************************************************
changed: [192.168.0.218]

TASK [toch file2] ***************************************************************************************************
changed: [192.168.0.218]

PLAY RECAP **********************************************************************************************************
192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   

[root@test ~]#ansible 192.168.0.218 -m shell -a 'ls -l /root'
192.168.0.218 | SUCCESS | rc=0 >>
總用量 12
-rw-r--r--. 1 root   root    0 11月 17 16:49 abc
-rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
-rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file

[root@test ~]#

  說明:可看到我們定義在/etc/ansible/host_vars/下的主機變量文件中的變量生效了。

同理,我們要想針對某個組的主機定義一些變量,我們只需要在/etc/ansible/group_vars/目錄下創建與主機清單中的主機組同名的文件即可。

  三、使用高階變量

  對於普通變量,例如由ansible命令行設定的,hosts文件中定義的以及playbook中定義的和變量文件中定義的,這些變量都被稱為普通變量或者叫簡單變量,我們可以在playbook中直接用雙大括號加變量名來讀取變量內容;除此以外ansible還有數組變量或者叫做列表變量,如下所示:

[root@test ~]#cat vars.yml 
---
packages_list:
  - vsftpd
  - nginx
[root@test ~]#

  列表定義完成后我們要使用其中的變量可以列表名加下標的方式去訪問,有點類似shell腳本里的數組的使用,如下所示

[root@test ~]#cat test.yml 
---
- hosts: 192.168.0.218
  remote_user: root
  
  vars_files:
    - vars.yml
  tasks:
    - name: touch file
      file: name={{ packages_list[0] }} state=touch
    - name: mkdir dir
      file: name={{ packages_list[1] }} state=directory
[root@test ~]#

  說明:我們要使用列表中的第一個元素變量,我們可以寫成vars_list[0],使用第二個變量則下標就是1,依此類推

[root@test ~]#ansible *218 -m shell -a 'ls -l /root'
192.168.0.218 | SUCCESS | rc=0 >>
總用量 12
-rw-r--r--. 1 root   root    0 11月 17 16:49 abc
-rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
-rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file

[root@test ~]#ansible-playbook test.yml 

PLAY [192.168.0.218] ************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************
ok: [192.168.0.218]

TASK [touch file] ***************************************************************************************************
changed: [192.168.0.218]

TASK [mkdir dir] ****************************************************************************************************
changed: [192.168.0.218]

PLAY RECAP **********************************************************************************************************
192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   

[root@test ~]#ansible *218 -m shell -a 'ls -l /root'
192.168.0.218 | SUCCESS | rc=0 >>
總用量 16
-rw-r--r--. 1 root   root    0 11月 17 16:49 abc
-rw-r--r--. 1 root   root    0 11月 17 16:49 bcd
drwxr-xr-x. 2 root   root 4096 11月 17 17:23 nginx
drwxr-xr-x. 2 qiuhom root 4096 11月 11 19:18 scripts
drwxr-xr-x. 3 qiuhom root 4096 11月 11 19:28 test
-rw-r--r--. 1 root   root   57 11月 13 19:15 test_cron_file
-rw-r--r--. 1 root   root    0 11月 17 17:23 vsftpd

[root@test ~]#

  說明:可看到我們創建的文件和目錄在目標主機已經生成

上面的用法是典型的python列表的用法,在python中讀取列表中的元素就是用下標的表示來讀取相應的元素的值。接下我們將介紹另外一種更為複雜的變量,它類似python中的字典概念,但比字典的維度要高,更像是二維字典。ansible內置變量ansible_eth0就是這樣一種,它用來保存遠端主機上面eth0接口的信息,包括ip地址和子網掩碼等。如下所示

[root@test ~]#cat test.yml     
---
- hosts: 192.168.0.218
  remote_user: root
  
  tasks:
    - debug: var=ansible_eth0 
[root@test ~]#ansible-playbook test.yml 

PLAY [192.168.0.218] ************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************
ok: [192.168.0.218]

TASK [debug] ********************************************************************************************************
ok: [192.168.0.218] => {
    "ansible_eth0": {
        "active": true, 
        "device": "eth0", 
        "features": {
            "fcoe_mtu": "off [fixed]", 
            "generic_receive_offload": "on", 
            "generic_segmentation_offload": "on", 
            "highdma": "off [fixed]", 
            "large_receive_offload": "off [fixed]", 
            "loopback": "off [fixed]", 
            "netns_local": "off [fixed]", 
            "ntuple_filters": "off [fixed]", 
            "receive_hashing": "off [fixed]", 
            "rx_checksumming": "on", 
            "rx_vlan_filter": "on [fixed]", 
            "rx_vlan_offload": "on [fixed]", 
            "scatter_gather": "on", 
            "tcp_segmentation_offload": "on", 
            "tx_checksum_fcoe_crc": "off [fixed]", 
            "tx_checksum_ip_generic": "on", 
            "tx_checksum_ipv4": "off", 
            "tx_checksum_ipv6": "off", 
            "tx_checksum_sctp": "off [fixed]", 
            "tx_checksum_unneeded": "off", 
            "tx_checksumming": "on", 
            "tx_fcoe_segmentation": "off [fixed]", 
            "tx_gre_segmentation": "off [fixed]", 
            "tx_gso_robust": "off [fixed]", 
            "tx_lockless": "off [fixed]", 
            "tx_scatter_gather": "on", 
            "tx_scatter_gather_fraglist": "off [fixed]", 
            "tx_tcp6_segmentation": "off", 
            "tx_tcp_ecn_segmentation": "off", 
            "tx_tcp_segmentation": "on", 
            "tx_udp_tnl_segmentation": "off [fixed]", 
            "tx_vlan_offload": "on [fixed]", 
            "udp_fragmentation_offload": "off [fixed]", 
            "vlan_challenged": "off [fixed]"
        }, 
        "hw_timestamp_filters": [], 
        "ipv4": {
            "address": "192.168.0.218", 
            "broadcast": "192.168.0.255", 
            "netmask": "255.255.255.0", 
            "network": "192.168.0.0"
        }, 
        "ipv6": [
            {
                "address": "fe80::20c:29ff:fee8:f67b", 
                "prefix": "64", 
                "scope": "link"
            }
        ], 
        "macaddress": "00:0c:29:e8:f6:7b", 
        "module": "e1000", 
        "mtu": 1500, 
        "pciid": "0000:02:01.0", 
        "promisc": false, 
        "speed": 1000, 
        "timestamping": [
            "rx_software", 
            "software"
        ], 
        "type": "ether"
    }
}

PLAY RECAP **********************************************************************************************************
192.168.0.218              : ok=2    changed=0    unreachable=0    failed=0   

[root@test ~]#

  說明:以上playbook就實現了對ansible_eth0這個變量進行調試並打印,可以看到ansible_eth0是一個相對比較複雜的變量,裡面包含了字典,列表混合一起的一個大字典。

我們可以看到ansible_eht0裡面包含了很多內容,我們要想讀取其中的IPV4地址,我們可以採用“.”或者下標的方式去訪問,如下所示

[root@test ~]#cat test.yml 
---
- hosts: 192.168.0.218
  remote_user: root
  
  tasks:
    - name: print ipv4  
      shell: echo {{ ansible_eth0["ipv4"]["address"] }} 
    - name: print mac
      shell: echo  {{ ansible_eth0.macaddress }}
[root@test ~]#ansible-playbook test.yml -v
Using /etc/ansible/ansible.cfg as config file

PLAY [192.168.0.218] ************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************
ok: [192.168.0.218]

TASK [print ipv4] ***************************************************************************************************
changed: [192.168.0.218] => {"changed": true, "cmd": "echo 192.168.0.218", "delta": "0:00:00.001680", "end": "2019-11-17 18:30:21.926368", "rc": 0, "start": "2019-11-17 18:30:21.924688", "stderr": "", "stderr_lines": [], "stdout": "192.168.0.218", "stdout_lines": ["192.168.0.218"]}

TASK [print mac] ****************************************************************************************************
changed: [192.168.0.218] => {"changed": true, "cmd": "echo 00:0c:29:e8:f6:7b", "delta": "0:00:00.001746", "end": "2019-11-17 18:30:22.650541", "rc": 0, "start": "2019-11-17 18:30:22.648795", "stderr": "", "stderr_lines": [], "stdout": "00:0c:29:e8:f6:7b", "stdout_lines": ["00:0c:29:e8:f6:7b"]}

PLAY RECAP **********************************************************************************************************
192.168.0.218              : ok=3    changed=2    unreachable=0    failed=0   

[root@test ~]#

  說明:由此可以看出ansible多級變量的調用,使用中括號和點號都是可以的

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

X-Admin&ABP框架開發-RBAC

  在業務系統需求規劃過程中,通常對於諸如組織機構、用戶和角色等這種基礎功能,通常是將這部分功能規劃到通用子域中,這也說明了,對於這部分功能來講,是系統的基石,整個業務體系是建立於這部分基石之上的,當然,還有諸如多語言、設置管理、認證和授權等。對於這部分功能,ABP中存在這些概念,並且通過Module Zero模塊完成了這些概念。

 

一、角色訪問控制之RBAC

  RBAC:Role Based Access Control,基於角色的訪問控制,這在目前大多數軟件中來講已經算得上是普遍應用了,最常見的結構如下,結構簡單,設計思路清晰。

  

  但是也存在其它升級版的設計,諸如用戶權限表、角色組、用戶組的概念等,具體分類有RBAC0、RBAC1、RBAC2等,後者功能越來越強大,也越來越複雜。

  • RBAC0:是RBAC的核心思想。
  • RBAC1:是把RBAC的角色分層模型。
  • RBAC2:增加了RBAC的約束模型。
  • RBAC3:整合RBAC2 + RBAC1。

 

二、ABP中的RBAC

  在Abp中,已經集成了這些概念,並在ModuleZero模塊中實現了這些概念,基於IdentityServer4的ModuleZero模塊完成了封裝。對於我們大多數以業務為中心的開發人員來講,不應該又去造一個輪子,而是應該開好這輛車。首先看下Abp中的RBAC模型

  

  在這其中權限表中記錄了用戶與權限,角色與權限兩部分。對於權限通常指的是功能權限和數據權限兩部分,一般來講,大多指的是功能權限,這種通過角色與權限進行管理即可,如還有用戶部分的功能區分,則可以再使用上用戶與權限,而對於數據權限,可以利用用戶與權限部分,個人用的比較少,但是,可以想象到這麼一個場景,針對於一家門店內的多個店長,角色相同即相應的權限相同,但各自關心的數據來源不同,關心東部、南部等數據,而不關心西部、北部數據,因此可以在數據層面進行劃分,比如設置數據來源,東南西北,對於數據來源進行權限關聯,這樣一來用戶本身如果擁有東部數據權限,則只能看到東部數據。

 

1、權限聲明及應用

  在Abp中,需要首先在Core層/Authorization/PermissionNames.cs中聲明權限,Abp權限部分設計原則是:先聲明再使用

/// <summary>
/// 權限命名
/// </summary>
public static class PermissionNames
{
    #region 頂級權限
    public const string Pages = "Pages";
    #endregion

    #region 基礎支撐平台
    public const string Pages_Frame = "Pages.Frame";

    #region 租戶管理
    public const string Pages_Frame_Tenants = "Pages.Frame.Tenants";
    #endregion

    #region 組織機構
    public const string Pages_Frame_OrganizationUnits = "Pages.Frame.OrganizationUnits";
    public const string Pages_Frame_OrganizationUnits_Create = "Pages.Frame.OrganizationUnits.Create";
    public const string Pages_Frame_OrganizationUnits_Update = "Pages.Frame.OrganizationUnits.Update";
    public const string Pages_Frame_OrganizationUnits_Delete = "Pages.Frame.OrganizationUnits.Delete";
    #endregion

    #region 用戶管理
    public const string Pages_Frame_Users = "Pages.Frame.Users";
    public const string Pages_Frame_Users_Create = "Pages.Frame.Users.Create";
    public const string Pages_Frame_Users_Update = "Pages.Frame.Users.Update";
    public const string Pages_Frame_Users_Delete = "Pages.Frame.Users.Delete";
    public const string Pages_Frame_Users_ResetPassword = "Pages.Frame.Users.ResetPassword";
    #endregion

    #region 角色管理
    public const string Pages_Frame_Roles = "Pages.Roles";
    public const string Pages_Frame_Roles_Create = "Pages.Frame.Roles.Create";
    public const string Pages_Frame_Roles_Update = "Pages.Frame.Roles.Update";
    public const string Pages_Frame_Roles_Delete = "Pages.Frame.Roles.Delete";
    #endregion

}

  然後在Core層/Authorization/XXXAuthorizationProvider.cs中設置具體權限,在此處設置權限時,可以根據權限設計時候的職責劃分,比如如果僅僅是多租戶需要這部分,那便設置權限範圍為多租戶即可。

public class SurroundAuthorizationProvider : AuthorizationProvider
{
    public override void SetPermissions(IPermissionDefinitionContext context)
    {
        #region 頂級權限
        var pages = context.CreatePermission(PermissionNames.Pages, L("Pages"));
        #endregion

        #region 基礎支撐平台
        var frame = pages.CreateChildPermission(PermissionNames.Pages_Frame, L("Frame"));

        #region 租戶管理
        frame.CreateChildPermission(PermissionNames.Pages_Frame_Tenants, L("Tenants"), multiTenancySides: MultiTenancySides.Host);
        #endregion

        #region 組織機構
        var organizationUnits = frame.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits, L("OrganizationUnits"));
        organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Create, L("CreateOrganizationUnit"));
        organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Update, L("EditOrganizationUnit"));
        organizationUnits.CreateChildPermission(PermissionNames.Pages_Frame_OrganizationUnits_Delete, L("DeleteOrganizationUnit"));
        #endregion

        #region 用戶管理
        var users = frame.CreateChildPermission(PermissionNames.Pages_Frame_Users, L("Users"));
        users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Create, L("CreateUser"));
        users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Update, L("UpdateUser"));
        users.CreateChildPermission(PermissionNames.Pages_Frame_Users_Delete, L("DeleteUser"));
        users.CreateChildPermission(PermissionNames.Pages_Frame_Users_ResetPassword, L("ResetPassword"));
        #endregion

        #region 角色管理
        var roles = frame.CreateChildPermission(PermissionNames.Pages_Frame_Roles, L("Roles"));
        roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Create, L("CreateRole"));
        roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Update, L("UpdateRole"));
        roles.CreateChildPermission(PermissionNames.Pages_Frame_Roles_Delete, L("DeleteRole"));
        #endregion
    }
}

  在設置完畢后,需要將該類集成到Core層/XXXCoreModule當前模塊中,才能使得該部分權限設置生效。

//配置權限管理
Configuration.Authorization.Providers.Add<SurroundAuthorizationProvider>();

   作為業務的入口,菜單是較為直觀的體現方式,現在可以,為菜單分配權限了,擁有權限的人才能看的到菜單,同時後台方法中也要有權限判定,菜單僅作為前端入口上的控制,權限判定作為後端的控制。在MVC層的Startup/XXXNavigationProvider.cs中完成菜單的配置工作,可以配置多級菜單,每個菜單可以配置相應的權限,在生成菜單判定時,如果父級菜單權限不足,則直接會跳過子級菜單的判定。

new MenuItemDefinition(//基礎支撐
    PageNames.FrameManage,
    L(PageNames.FrameManage),
    icon: "&#xe828;",
    requiredPermissionName: PermissionNames.Pages_Frame
).AddItem(
    new MenuItemDefinition(//組織機構
        PageNames.OrganizationUnits,
        L(PageNames.OrganizationUnits),
        url: "/OrganizationUnits",
        icon: "&#xe6cb;",
        requiredPermissionName: PermissionNames.Pages_Frame_OrganizationUnits
    )
).AddItem(
    new MenuItemDefinition(//用戶管理
        PageNames.Users,
        L(PageNames.Users),
        url: "/Users",
        icon: "&#xe6cb;",
        requiredPermissionName: PermissionNames.Pages_Frame_Users
    )
).AddItem(
    new MenuItemDefinition(//角色管理
        PageNames.Roles,
        L(PageNames.Roles),
        url: "/Roles",
        icon: "&#xe6cb;",
        requiredPermissionName: PermissionNames.Pages_Frame_Roles
    )
).AddItem(
    new MenuItemDefinition(//系統設置
        PageNames.HostSettings,
        L(PageNames.HostSettings),
        url: "/HostSettings",
        icon: "&#xe6cb;",
        requiredPermissionName: PermissionNames.Pages_Frame_HostSettings
    )
)

  在前端頁面上,對於按鈕級別的控制也通過權限判定,Abp提供了判定方法,利用Razor語法進行按鈕控制

@if (await PermissionChecker.IsGrantedAsync(PermissionNames.Pages_Core_DataDictionary_Create))
{
    <button class="layui-btn layuiadmin-btn-dataDictionary" data-type="addDataDictionary">添加類型</button>
}

  在後端方法上,通常我喜歡直接在應用服務中的方法上做權限判定(當然也可以前移到MVC層,但是這樣一來,針對於WebApi形式的Host層,又得多加一次判定了),利用AbpAuthorize特性,判定該方法需要哪幾個權限才能訪問,而在mvc的控制器上做訪問認證。

[AbpAuthorize(PermissionNames.Pages_Core_DataDictionary_Create)]
private async Task CreateDataDictionaryAsync(CreateOrUpdateDataDictionaryInput input)
{

}

 

2、角色與權限

   在Abp中,角色信息存儲在abprole表中,角色與權限間的關聯存儲在abppermission這張表中,一個角色有多個權限,如果某個角色的權限被去掉了,這張表中的相關記錄將由abp負責刪除,我們只需要完成掌控哪些權限是這個角色有的就行。Abp中已經完成了角色的所有操作,但是前端部分採用的是bootstrap弄的,將其改造一波,成為layui風格。

  

  在創建角色中,主要是將選中的權限掛鈎到具體的某個角色上,該部分代碼沿用abp中自帶的角色權限處理方法。

private async Task CreateRole(CreateOrUpdateRoleInput input)
{
    var role = ObjectMapper.Map<Role>(input.Role);
    role.SetNormalizedName();

    CheckErrors(await _roleManager.CreateAsync(role));

    var grantedPermissions = PermissionManager
        .GetAllPermissions()
        .Where(p => input.PermissionNames.Contains(p.Name))
        .ToList();

    await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
}

  指定角色Id,租戶Id及之前聲明的權限名稱,在abppermission中可查看到具體角色權限。

  

 

3、用戶與角色

   一個用戶可以承擔多個角色,履行不同角色的義務,作為一個業務系統最基本的單元,abp中提供了這些概念並在Module Zero模塊中已經完成了對用戶的一系列操作,用戶信息存儲在AbpUsers表中,用戶直接關聯的角色保存在AbpUserRoles表中,abp中MVC版本採用的是bootstrap風格,因此,用layui風格完成一次替換,並且,改動一些頁面布局。

  

  Abp版本中,由於是土耳其大佬所開發的習慣,針對於姓和名做了拆分,因此對於我們的使用要做一次處理,我這先簡單處理了一下,並且在業務系統中,郵箱時有時無,因此也需要進行考慮。

[AbpAuthorize(PermissionNames.Pages_Frame_Users_Create)]
private async Task CreateUser(CreateOrUpdateUserInput input)
{
    var user = ObjectMapper.Map<User>(input.User);
    user.TenantId = AbpSession.TenantId;
    user.IsEmailConfirmed = true;
    user.Name = "Name";
    user.Surname = "Surname";
    //user.EmailAddress = string.Empty;

    await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
    foreach (var validator in _passwordValidators)
    {
        CheckErrors(await validator.ValidateAsync(UserManager, user, AppConsts.DefaultPassword));
    }

    user.Password = _passwordHasher.HashPassword(user, AppConsts.DefaultPassword);

    await _userManager.InitializeOptionsAsync(AbpSession.TenantId);

    CheckErrors(await _userManager.CreateAsync(user, AppConsts.DefaultPassword));

    if (input.AssignedRoleNames != null)
    {
        CheckErrors(await _userManager.SetRoles(user, input.AssignedRoleNames));
    }

    if (input.OrganizationUnitIds != null)
    {
        await _userManager.SetOrganizationUnitsAsync(user, input.OrganizationUnitIds);
    }

    CurrentUnitOfWork.SaveChanges();
}

  此處對用戶個人單獨的權限沒有去做處理,依照Abp的文檔有那麼一句話,大多數應用程序中,基於角色的已經足夠使用了,如果想聲明特定權限給用戶,那麼針對於用戶本身的角色權限則被覆蓋。    

 

 至此,修改整合用戶、角色和權限加入到系統中初步完成了,至於一些更為豐富的功能,待逐步加入中,車子再好,司機也得睡覺。

 

 倉庫地址:

2019-11-17,望技術有成后能回來看見自己的腳步

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享