Gradle 实现 Android 多渠道定制化打包

版权声明:本文为博主原创文章,未经博主允许不得转载。

最近在项目中遇到需要实现 Apk 多渠道、定制化打包, Google 、百度查找了一些资料,成功实现了上述功能,在此记录以备不时之需,温故而知新,可以为师矣~

需求可以总结如下:

如何实现多个 Apk 安装在同一设备

在之前的印象中,同一个应用在同一设备上只能安装一个,除非手动修改 AndroidManifest.xml 文件中的包名( package ),但这么做的后果就是新的应用真的是新的应用,旧版应用再也收不到更新。而现在你通过 Gradle,你可以轻松构建多个不同版本的应用,并且在同一设备上安装使用。

这里要用到 productFlavors ,productFlavors 可以用来自定义应用构建版本,我们可以用其 applicationId 属性来实现多个 Apk 安装在同一设备上。

build.gradle 中部分配置代码如下:

android {
compileSdkVersion 24
buildToolsVersion "24.0.1" //默认配置,所有 productFlavors 都会继承 defaultConfig 中配置的属性
defaultConfig {
//默认的 applicationId,一般与 AndroidManifest.xml 文件 package属性相同
applicationId "com.littlejie.multichannel"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
} // productFlavors 定义了一个应用的自定义构建版本
//一个单一的项目可以同时定义多个不同的 flavor 来改变应用的输出。
// productFlavors 这个概念是为了解决不同的版本之间的差异非常小的情况,通常用于区分同一个应用的不同渠道/客户等,可包含少量业务功能差别。
// productFlavors 中的 flavor 不能跟 buildType 中的一样,否则会报: "ProductFlavor names cannot collide with BuildType names"
productFlavors { //默认版本,不设置 applicationId ,继承 defaultConfig 中的配置
flavors_default {
} //开发版本, applicationId 替换为 com.littlejie.multichannel.dev
flavors_dev {
applicationId "com.littlejie.multichannel.dev"
} //发布版本, applicationId 替换为 com.littlejie.multichannel.release
flavors_release {
applicationId "com.littlejie.multichannel.release"
}
}
}

MainActivity.java:

public class MainActivity extends Activity {

    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Log.d(TAG, "package name = " + this.getPackageName());
}
}

在 Android Studio 中执行如下命令:

//打 debug 包,gradle 命令会在后面 `gradle task`中详细讲述
gradle clean assembleDebug

打包完成后,将 Apk 安装到模拟器(adb install name.apk),运行,log 如下:

flavors_default:

09-17 22:43:55.390 19747-19747/com.littlejie.multichannel D/MainActivity: package name = com.littlejie.multichannel

flavors_dev:

09-17 22:11:30.860 2638-2638/com.littlejie.multichannel.dev D/MainActivity: package name = com.littlejie.multichannel.dev

flavors_release:

09-17 22:44:55.610 20650-20650/com.littlejie.multichannel.release D/MainActivity: package name = com.littlejie.multichannel.release

从这里可以看出,不同 flavor 的 package name 被 applicationId 替换掉了,而且同一个模拟器上可以同时安装以上三个应用。

下面我们再看看 AndroidManifest.xml 中发生了什么变化。这里需要用到 aapt 来查看 AndroidManifest.xml 的信息:

//输出 apk 的 AndroidManifest.xml 文件的信息
aapt dump xmltree ***.apk AndroidManifest.xml

关于 aapt 使用的更多用法,可以阅读这篇博文:使用 aapt 查看 apk 的各种信息

下面是 flavors_dev 版本的信息,可以看出 Java 源文件的包名并没有发生改变,而 package 属性的值被替换为 applicationId了。

如果在申请第三方 SDK 接入,则对应的包名应该填 applicationId ,而不是 AndroidManifest.xml 中的默认值

lishengjiedeMacBook-Pro:apk littlejie$ aapt dump xmltree multichannel-flavors_dev-debug.apk AndroidManifest.xml
N: android=http://schemas.android.com/apk/res/android
E: manifest (line=2)
A: android:versionCode(0x0101021b)=(type 0x10)0x1
A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
//此处 package 的值已替换成 applicationId 的值
A: package="com.littlejie.multichannel.dev" (Raw: "com.littlejie.multichannel.dev")
A: platformBuildVersionCode=(type 0x10)0x18 (Raw: "24")
A: platformBuildVersionName=(type 0x4)0x40e00000 (Raw: "7.0")
E: uses-sdk (line=7)
A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x18
E: application (line=11)
A: android:theme(0x01010000)=@0x7f08008e
A: android:label(0x01010001)=@0x7f060020
A: android:icon(0x01010002)=@0x7f030000
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff
// Activity 的包名还是原来 AndroidManifest.xml 中申明的
E: activity (line=17)
A: android:name(0x01010003)="com.littlejie.multichannel.MainActivity" (Raw: "com.littlejie.multichannel.MainActivity")
E: intent-filter (line=18)
E: action (line=19)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=21)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")

applicationId 的原理可以理解为在 gradle 打包的时,动态合并属性,将 package 替换为 applicationId 指定的值,但并不会替换 Java 文件的包名,包括生成的 R 文件(可以去对应 module 下的 build/generated 目录下查看对应 flavor 的 R 文件)。

另外,由于最终生成的包中 AndroidManifest.xml 文件中的 package 属性被 applicationId 替换掉,故对于某些第三方 SDK ,如:微信、高德地图等需要验证包名的,就会碰到相当蛋疼的事,每个包都需要重新去生成 APPID 和 APPKEY,如果渠道很多,那么像微信就会出现问题微信账号申请的应用数就会超出微信的限制。

Android 官方文档原文如下:

Therefore, we have decoupled the two usages of package name:

The final package that is used in your built .apk's manifest, and is the package your app is known as on your device and in the Google Play store, is the "application id".

The package that is used in your source code to refer to your R class, and to resolve any relative activity/service registrations, continues to be called the "package".

补充ApplicationId versus PackageName

替换 AndroidManifest.xml 中的属性

这里可以参考友盟统计 SDK 中使用的方案。该方案通过在 AndroidManifest.xml 文件中 application 标签下指定 <mate-data> 设置占位符来实现动态替换属性值。

<meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL}" />

占位符形如${name},在最终执行 AndroidManifest.xml 文件合并的时候,占位符会被 build.gradle 中对应值取代。 build.gradle 的配置需要用到上节讲到的 productFlavors 的 manifestPlaceholders 属性, manifestPlaceholders 属性直译过来就是清单文件占位符

下面是 build.gradle 的节选代码:

productFlavors {

   //将 AndroidManifest.xml 文件中的 ${UMENG_CHANNEL} 替换为 default
flavors_default {
manifestPlaceholders = [UMENG_CHANNEL: "defalut"]
} flavors_dev {
applicationId "com.littlejie.multichannel.dev"
manifestPlaceholders = [UMENG_CHANNEL: "dev"]
} flavors_release {
applicationId "com.littlejie.multichannel.release"
manifestPlaceholders = [UMENG_CHANNEL: "release"]
} }

如果你要替换多个属性,则只需要将 manifestPlaceholders 的写法如下:

manifestPlaceholders = [VALUE_NAME1 : "value" , VALUE_NAME2 : "value"]

补充:关于 AndroidManifest 文件合并规则可以查看 官方文档

替换资源文件

多渠道打包的时候可能会碰到这种情况:每个应用市场的启动页图标、应用名称可能会有点小出入,更有甚者,连布局都不一样。这时候我们该怎么办呢?

有一种解决办法就是:在代码里进行判断,根据渠道的不一样,加载不同的图片和布局,这是一种解决办法。但是当渠道有很多时,代码就会变得很难维护,而且指定渠道用到的资源文件都会被打入所有 Apk 中。所以这个方法并不值得推荐。那么,有什么好的解决办法呢?

办法 Google 早就给我们想好了,而且相当简单,那就是:在 main 的同级目录下创建以渠道名命名的文件夹,然后创建资源文件(路径要与 main 中的一致),然后打包的时候 gradle 就会自己替换或者合并资源。

例如, App 的默认 icon 路径为 main\res\mipmap-hdpi\ic_launcher.png ,那么 flavors_dev的路径就为 flavors_dev\res\mipmap-hdpi\ic_launcher.png ,打包 flavors_dev 渠道的时候会自动替换图片。

对于资源合并,如果在 main 下的 strings.xml 内容为:

<resources>
<string name="app_name">MultiChannel</string>
<string name="string_merge">我是string,我暂时没被合并</string>
</resources>

在 flavors_dev 下的 strings.xml 内容为:

<resources>
<string name="string_merge">我是dev_string,我会把string合并</string>
</resources>

当打 flavors_dev 渠道包时,最终 strings.xml 会变成:

<resources>
<string name="app_name">MultiChannel</string>
<string name="string_merge">我是dev_string,我会把string合并</string>
</resources>

以上特性可以用来替换 Apk 的应用名称和应用图标,这比使用前面讲到的占位符方便很多。同理,替换图片和合并颜色的原理也相似。

多渠道使用独立签名

多渠道打包的时候,可能每个渠道包的签名都必须不一样,真正做到定制化,那么,怎么实现每个渠道包使用指定的签名呢?

平时我们打包的时候是这样的:

signingConfigs {
release {
storeFile file("签名文件路径")
storePassword "storePassword"
keyAlias "keyAlias"
keyPassword "keyPassword"
} } buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
shrinkResources true
//指定打 release 包时使用的签名文件
signingConfig signingConfigs.release
} //如果 debug 包需要测试诸如微信、地图等第三方 sdk ,则可以指定 debug 包使用 release 包的签名
//debug {
// signingConfig signingConfigs.release
//}
}

而给每个渠道包指定签名其实也差不多。

Google 官方原话:

This enables either having all release packages share the same SigningConfig, by setting android.buildTypes.release.signingConfig, or have each release package use their own SigningConfig by setting each android.productFlavors.*.signingConfig objects separately.

大意就是,在 buildType 下指定签名的具体属性,形如 android.productFlavors.*.signingConfig signingConfigs.* ,前一个 * 指代在 productFlavors 中定义的 flavor ,后一个 * 指代在 signingConfigs 定义的属性。值得注意的是,signingConfigs 必须定义在 buildType 之前。

以下是 build.gradle 的配置节选:

//定义签名属性
signingConfigs {
flavors_default {
//如果签名文件在项目的根目录下,则可以这么写
storeFile file("../littlejie.jks")
storePassword "******"
keyAlias "******"
keyPassword "*****"
} flavors_dev {
storeFile file("../littlejie_dev.jks")
storePassword "*****"
keyAlias "*****"
keyPassword "*****"
}
} buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
shrinkResources true //多个 flavor ,指定 flavor 使用指定 签名
productFlavors.flavors_default.signingConfig signingConfigs.flavors_default
productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev
} //如果 debug 包需要测试诸如微信、地图等第三方 sdk ,则可以指定 debug 包使用 release 包的签名
//debug 并不能设置多个签名
//debug {
// productFlavors.flavors_default.signingConfig signingConfigs.flavors_default
// productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev
//}
}

下面我们来验证下生成的包的签名是否正确,查看签名我们会用到如下两个命令:

//查看签名文件的属性
keytool -list -keystore 签名文件 //查看 apk 的签名,需要提前解压 apk ,获取 CERT.RSA(位于解压目录下 /META-INF 下)
//以下命令行是在 apk 解压目录下执行
keytool -printcert -file META-INF/CERT.RSA

更多 keytool 命令使用可以查看 官方文档

首先,我们来看下 littlejie.jks 的信息:

lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie.jks
输入密钥库口令: 密钥库类型: JKS
密钥库提供方: SUN 您的密钥库包含 1 个条目 littlejie, 2016-9-18, PrivateKeyEntry,
证书指纹 (SHA1): A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84

解压 multichannel-flavors_default-release.apk ,查看 CERT.RSA 信息

lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_default-release/META-INF/CERT.RSA
所有者: CN=littlejie
发布者: CN=littlejie
序列号: 71693e05
有效期开始日期: Sun Sep 18 17:20:34 CST 2016, 截止日期: Thu Sep 12 17:20:34 CST 2041
证书指纹:
MD5: AC:12:83:51:44:FC:82:68:8B:23:7B:E9:12:24:AE:52
SHA1: A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84
SHA256: AD:04:19:5F:92:00:0D:FA:7C:E5:8A:12:57:72:4C:1E:0E:2E:FC:0D:92:28:05:D0:CC:42:FC:93:95:44:88:88
签名算法名称: SHA256withRSA
版本: 3

可以发现两者的 SHA1 值是相等的。

同理,可以查看 littlejie_dev.jks 和 multichannel-flavors_dev-release.apk 的签名信息

//littlejie_dev.jks 的签名信息
lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie_dev.jks
输入密钥库口令: 密钥库类型: JKS
密钥库提供方: SUN 您的密钥库包含 1 个条目 littlejie, 2016-9-18, PrivateKeyEntry,
证书指纹 (SHA1): B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2 //multichannel-flavors_dev-release.apk 的签名信息
lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_dev-release/META-INF/CERT.RSA
所有者: CN=littlejie
发布者: CN=littlejie
序列号: 48346e15
有效期开始日期: Sun Sep 18 17:21:23 CST 2016, 截止日期: Thu Sep 12 17:21:23 CST 2041
证书指纹:
MD5: 15:E9:E1:67:AB:33:8B:04:A4:C3:D0:05:8F:A6:35:37
SHA1: B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2
SHA256: 96:A5:14:EC:28:25:32:0D:3E:D0:DB:D0:84:06:E7:9C:17:D7:91:83:A4:51:93:AB:34:3E:D9:FD:C5:FA:A1:8E
签名算法名称: SHA256withRSA
版本: 3

但是这里有个问题,就是这种给某个 flavor 指定签名的方法对 debug 无效,有兴趣的同学可以看上述注释掉的 debug 签名部分配置。简单来说,debug 签名只能指定一个或者使用默认的 debug 签名。

若哪位大神有解决方案,欢迎指出~

这里再做几点补充:

  1. 多渠道使用独立签名,打包时千万不要使用 Android Studio 中 Build 菜单下的 Generate Signed APK,因为当你使用这个打包的时候, Android Studio 会让你指定使用的签名文件, so 你就等着哭吧~楼主因为这个折腾了半天。解决方法就是使用 gradle tasks。传送门:Android Gradle Build Tasks
  2. 鉴于第一点中的传送门需要翻墙,所以在这里简单介绍一下 Android Gradle Build Tasks 的使用。
    • 打全部包: gradle assemble
    • 打全部 Debug 包: gradle assembleDebug ,可以简写为 gradle aD ,前提是没有相同缩写的参数
    • 打全部 Release 包: gradle assembleRelease,可以简写为 gradle aR
    • 打指定 flavor 包: gradle assemble(flavor)(Debug|Release)
    • 打包完成后安装(设备上没有安装该 apk ,否则会失败,而且只能指定 flavor ,不然也会失败): gradle install(flavor)(Debug|Release)
    • 打包前先 clean 一下(在测试的时候很必要,如果不 clean 的话,可能会导致某些小修改不会及时打入新包): gradle clean assembleDebug

利用 Gradle 修改构建版本号

楼主表示对 Groovy 不是很熟,所以利用 Gradle 自动修改构建版本这个就先留着,我先去研究几天~

补充

有童鞋在评论中说:使用 productFlavors 打包效率太低,的确是这样, gradle 好用是好用,就是打包效率低。如果只是单纯生成渠道包,建议使用美团多渠道打包方案,另外 360 加固也是一种不错的选择,效率都比使用 gradle 来的高。但如果需要替换 Apk 中的图片、字符串、应用的 applicationId 、给指定渠道的包使用指定的签名,那么只能乖乖使用 gradle 打包了,慢你也得忍着~

之前刚开始调研的时候,发现 Github 上有个 ApkCustomizationTool 项目,它是通过对 Apk 解包,替换图片、字符串,然后重新签名,不过这毕竟是事后诸葛亮,控制在打包的源头总是毕竟好的,有兴趣的同学可以去研究下。

不知大家有没有这种感受,每次发版上传渠道的时候想死有没有?o(╯□╰)o

总结

以上就是自己在使用 Gradle 实现 Android 多渠道打包时碰到的问题, Android 官方关于使用 Gradle 的文档已经很详细了,自己总结的只是一点皮毛,有时间要去自习研读下。

工作一年多,愣是没有写博客做总结,好多东西都是用过就忘,下次要用再找,没有成体系的 Android 知识结构,对工资不满意,可就连想跳槽面试都没底气。这次写这篇博客画了思维导图,自以为逻辑清晰了,可是真正要把这些东西讲述清楚,还真是一件麻烦的事看来,自己还有很长的路要走

这段时间自己也在思考,是转行还是去考事业编制,还是继续做 Android。转行,除了编程自己好像别的什么也不会,当然自己编程也做的不怎么好。考事业编制,这个可以考虑,毕竟再很多人眼里这是个旱涝保收的职业。继续做 Android ,这个也不错,除了每次都花大把时间用来改 UI,别的都还不错(吐槽产品)。

话说,有没有什么工作,自由、上班时间少、工资高的?当然没有,至少现阶段的自己是接触不到的,所以,骚年,还是努力吧!多读书、多看报、多运动,少吃零食多睡觉~

恩,算是对工作一年多的总结也是吐槽~

读万卷书,行万里路~

参考

  1. Gradle Plugin User Guide
  2. Android Plugin DSL Reference
  3. Android Studio Gradle实践之多渠道自动化打包+版本号管理

Gradle 实现 Android 多渠道定制化打包的更多相关文章

  1. Maven 教程(20)— 使用maven-assembly-plugin插件来定制化打包

    原文地址:https://blog.csdn.net/liupeifeng3514/article/details/79777976 简单的说,maven-assembly-plugin 就是用来帮助 ...

  2. 用Gradle 构建android程序

    前言 android gradle 的插件终于把混淆代码的task集成进去了,加上最近,android studio 用的是gradle 来构建项目, 下定决心把android gralde 构建项目 ...

  3. Android自己主动化构建之Ant多渠道打包实践(下)

    前言 上一篇(Android自己主动化构建之Ant多渠道打包实践(上))已经介绍了Android的apk是怎样构建的,本篇博客继续Ant打包的实践过程. 集成友盟统计SDK 这里以友盟统计为例,对各个 ...

  4. 【Android】Android多渠道打包--Gradle打包

    Android多渠道打包--Gradle打包 前言 由于App一般都会在多个应用市场上架,为了分析App在每个不同渠道的具体的数据,一般都会对不同渠道打包不同的App.多渠道打包有多种方式,这里只介绍 ...

  5. 更便捷的Android多渠道打包方式

    本文先回顾了以往流行的多渠道打包方式,随后引入的mcxiaoke的packer-ng-plugin项目,介绍该项目在实际应用(配合友盟统计)中如何解决更方便的Android多渠道打包问题 多渠道打包方 ...

  6. Android 多渠道打包原理和使用

    每次中午吃饭总会和技术同学聊天.当做 iOS 开发的做安卓开发的人员在一起的时候,他们中间又多了一个话题:iOS 开发难还是安卓开发难. 这个时候做安卓开发的同学最激动说安卓开发要自己画界面.机型复杂 ...

  7. Android业务组件化之Gradle和Sonatype Nexus搭建私有maven仓库

    前言: 公司的业务组件化推进的已经差不多三四个月的时间了,各个业务组件之间的解耦工作已经基本完成,各个业务组件以module的形式存在项目中,然后项目依赖本地的module,多少有点不太利于项目的并行 ...

  8. android 多渠道打包

    android 多渠道打包 原理 在manifest文件中,application标签内部设置不同的metadata标签即可,可以通过java api获取这个matedata内的值 友盟提供的多渠道打 ...

  9. android studio 使用gradle 导出jar包,并打包assets目录

    警告:本文年久失修. 随着android studio的升级 ,gradle的升级,严格按照本文的代码去做可能不会成功,希望依然可以作为解决问题的思路. 最近项目在做一个sdk,供别的开发者使用,所以 ...

随机推荐

  1. 一起学 Java(三) 集合框架、数据结构、泛型

    一.Java 集合框架 集合框架是一个用来代表和操纵集合的统一架构.所有的集合框架都包含如下内容: 接口:是代表集合的抽象数据类型.接口允许集合独立操纵其代表的细节.在面向对象的语言,接口通常形成一个 ...

  2. 多线程的通信和同步(Java并发编程的艺术--笔记)

    1. 线程间的通信机制 线程之间通信机制有两种: 共享内存.消息传递.   2. Java并发 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式执行,通信的过程对于程序员来说是完全透 ...

  3. 分享两个BPM配置小技巧

    1.小技巧 流程图修改后发布的话版本号会+1,修改次数多了之后可能会导致版本号很高,这个时候可以将流程导出,然后删除对应的流程包再导入,发布数据模型和流程图之后,版本清零 2.小技巧 有的同事入职后使 ...

  4. 解决:SharePoint当中的STP网站列表模板没有办法导出到其它语言环境中使用

    首在在你的英文版本上,导出列表或是网站的模板,这个文件可能是这样滴:template.stp 把这个文件 template.stp 命名为 template.cab 解压 这个 *.cab 文件 在解 ...

  5. jira的插件开发流程实践

    怎么开头呢,由于自己比较懒,博客一直不怎么弄,以后克己一点,多传点自己遇到的问题和经历上来,供自己以后记忆,也供需要的小伙伴少走点弯路吧 最近公司项目需要竞标一个运维项目,甲方给予了既定的几种比较常用 ...

  6. TCP的数据传输小结

    TCP的交互数据流 交互式输入 通常每一个交互按键都会产生一个数据分组,也就是说,每次从客户传到服务器的是一个字节的按键(而不是每次一行) 经受时延的确认 通常TCP在接受到数据时并不立即发送ACK: ...

  7. Dubbo学习小记

    前言 周一入职的新公司,到了公司第一件事自然是要熟悉新公司使用的各种技术,搭建本地的环境. 熟悉新公司技术的过程中,首先就是Maven,这个前面已经写过文章了,然后就是Dubbo----公司的服务都是 ...

  8. Angular2学习笔记——Observable

    Reactive Extensions for Javascript 诞生于几年前,随着angular2正式版的发布,它将会被更多开发者所认知.RxJs提供的核心是Observable对象,它是一个使 ...

  9. 基于 SailingEase WinForm Framework 开发客户端程序(3:实现菜单/工具栏按钮的解耦及状态控制)

    本系列文章将详细阐述客户端应用程序的设计理念,实现方法. 本系列文章以  SailingEase WinForm Framework 为基础进行设计并实现,但其中的设计理念及方法,亦适用于任何类型的客 ...

  10. webpack搭建前端一条龙服务

    作为从grunt.gulp一路走来的老码农,一开始用webpack的时候我是很抗拒的.但由于核心库使用了vue,而webpack又是vue的最佳拍档(vue作者专门为其写了vue-loader),所以 ...