本文将从一个空目录开始搭建一个最小化可运行的完整angular项目。并且不依赖@angular/cli
,纯手工配置webpack
来实现。即花费巨大力气完成@angular/cli
中的如下命令:
ng new angulectron
ng build --prod
初始化
初始化项目使用yarn init
(或npm init
)完成,最终得到包含单个package.json
的项目。像这样:
mkdir angulectron && cd angulectron
yarn init
// 一路回车或细心输入配置
得到类似如下内容的package.json
文件:
{
"name": "angulectron",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
为了防止第一步太过简单,顺便我们再往里面添加一波依赖:
yarn add @angular/core @angular/common @angular/platform-browser @angular/platform-browser-dynamic @angular/compiler rxjs zone.js core-js
yarn add --dev @angular/compiler-cli webpack webpack-cli webpack-dev-server [email protected]
yarn add --dev html-webpack-plugin to-string-loader css-loader sass-loader raw-loader file-loader @ngtools/webpack @angular-devkit/build-optimizer uglifyjs-webpack-plugin mini-css-extract-plugin node-sass rimraf http-server
- 第一波依赖是angular相关的几个依赖,已经足够最简单运行了
- 第二波是大哥webpack还有干爹typescript。其中typescript必须指定2版本(目前最新已经到3以上了)。
- 第三波是webpack家族的一些loader和plugin,以及
rimraf
用于删除生成的文件,http-server
用于运行小服务器来访问生成的资源。
再添加一些脚本来帮助运行打包,这一步最终得到一个只包含单个package.json
文件的项目,内容像这样:
{
"name": "angulectron",
"version": "1.0.0",
"description": "Starter for electron app with angular(v6+)",
"main": "main.js",
"scripts": {
"http": "http-server ./wwwroot",
"prod": "rimraf wwwroot && yarn webpack -- --config ./webpack.config.js --open --progress --profile --content-base src/",
"dev": "yarn webpack-dev-server -- --config ./webpack.config.dev.js --open --progress --profile --watch --content-base src/",
"webpack": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js",
"webpack-dev-server": "node --max_old_space_size=4096 node_modules/webpack-dev-server/bin/webpack-dev-server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/yitimo/angulectron.git"
},
"keywords": [
"angular",
"electron"
],
"author": "yitimo ",
"license": "MIT",
"bugs": {
"url": "https://github.com/yitimo/angulectron/issues"
},
"homepage": "https://github.com/yitimo/angulectron#readme",
"devDependencies": {
"@angular-devkit/build-optimizer": "^0.8.4",
"@angular/cli": "^6.2.4",
"@angular/compiler-cli": "^6.1.9",
"@angular/language-service": "^6.1.9",
"@ngtools/webpack": "^6.2.4",
"@types/hammerjs": "^2.0.36",
"@types/node": "^10.11.5",
"@types/uglify-js": "^3.0.3",
"@types/webpack": "^4.4.14",
"codelyzer": "^4.5.0",
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^1.0.0",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"http-server": "^0.11.1",
"mini-css-extract-plugin": "^0.4.2",
"node-sass": "^4.9.3",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "^2.0.1",
"style-loader": "^0.23.0",
"to-string-loader": "^1.1.5",
"ts-loader": "^5.2.1",
"tslint": "^5.11.0",
"typescript": "^2.7.2",
"webpack": "^4.19.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8",
"webpack-inline-manifest-plugin": "^4.0.1"
},
"dependencies": {
"@angular/animations": "^6.1.9",
"@angular/common": "^6.1.9",
"@angular/compiler": "^6.1.9",
"@angular/core": "^6.1.9",
"@angular/forms": "^6.1.9",
"@angular/platform-browser": "^6.1.9",
"@angular/platform-browser-dynamic": "^6.1.9",
"@angular/platform-server": "^6.1.9",
"@angular/router": "^6.1.9",
"core-js": "^2.5.7",
"rxjs": "^6.3.3",
"zone.js": "^0.8.26"
}
}
最简单源代码
抛开脚手架,项目真正的源代码要放到一个src
目录下,本文重点不在此,所以除了入口文件外只创建最简单的源代码,只有一个根模块,根组件,以及外联的模板和样式文件,像这样:
polyfills.ts:
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
main.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app/app.module';
// webpack DefinePlugin 注入的变量,需要声明,否则编辑器会报错
declare var ENV: string;
if (ENV === 'production') {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
index.html:
My App
Loading...
<% if (isDevServer) { %><% } %>
app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule
],
declarations: [AppComponent],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }
app.component.ts:
import { Component } from '@angular/core';
@Component({
selector: 'angulectron',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css', 'app.component.scss']
})
export class AppComponent { }
app.component.html:
Hello !!!
app.component.css:
h2 {
color: #CD5C5C;
}
app.component.scss:
h2 {
font-size: 32px;
}
其中报错是因为未配置tsconfig.json
,编辑器报了es7
装饰器新特性的警告,所以在项目根目录新建一个tsconfig.json
:
{
"compilerOptions": {
"target": "es5",
"module": "esnext", // 重要 可以明显减小最终打包的体积
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"noEmitHelpers": true,
"importHelpers": true,
"strictNullChecks": false,
"lib": ["dom", "es2015"],
"baseUrl": "./src",
"paths": {
"@angular/*": ["../node_modules/@angular/*"]
}
},
"exclude": [
"node_modules",
"wwwroot"
]
}
webpack
webpack
配置是重头戏,实际上@angular/cli
生成的项目内部也使用了webpack
,不过已经被封装好了使用时完全不用去关心。
基本的webpack
配置为一个名为webpack.config.js
的文件,文件导出一个object
,这个object
至少包含以下几部分:
- entry 指定入口文件,我们这里是
main.ts
和polyfills.ts
这两个 - output 指定输出的目录、名字等
- module.rules 配置各种
loader
- plugins 配置各种额外规则
- optimization 配置资源压缩以及分包
- devServer 配置开发服务器
针对angular
项目特有的配置项是两个loader
和一个plugin
,均来自于@ngtools/webpack
这个包,实际上@angular/cli
内部也使用了这个东西,依靠这个包才让手动配置angular
项目的webpack
变得简单(对比angular2刚发布的年代)。
最终一个完整的webpack配置像这样:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const ENV = 'production';
module.exports = {
mode: ENV,
devtool: 'source-map',
entry: {
polyfills: path.resolve(__dirname, './src/polyfills.ts'),
main: path.resolve(__dirname, './src/main.ts')
},
output: {
path: path.resolve(__dirname, 'wwwroot'),
filename: '[name].[chunkhash].bundle.js',
sourceMapFilename: '[file].map',
chunkFilename: '[name].[chunkhash].chunk.js'
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
module: {
rules: [
{
test: /\.css$/,
use: ['to-string-loader', 'css-loader'],
exclude: [path.resolve(__dirname, './src/styles')]
},
{
test: /\.scss$/,
use: ['to-string-loader', 'css-loader', 'sass-loader'],
exclude: [path.resolve(__dirname, './src/styles')]
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
include: [path.resolve(__dirname, './src/styles')]
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
include: [path.resolve(__dirname, './src/styles')]
},
{
test: /\.html$/,
use: ['raw-loader'],
exclude: [path.resolve(__dirname, './src/index.html')]
},
{
test: /\.(jpg|png|gif|pdf|eot|woff2?|svg|ttf)$/,
use: 'file-loader'
},
{
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
use: [{
loader: '@angular-devkit/build-optimizer/webpack-loader',
options: {
sourceMap: false
}
}, '@ngtools/webpack']
},
{
test: /\.js$/,
use: [{
loader: '@angular-devkit/build-optimizer/webpack-loader',
options: {
sourceMap: false
}
}]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'body',
xhtml: true,
minify: true
}),
new DefinePlugin({
'isDevServer': 'false',
'ENV': JSON.stringify(ENV)
}),
new AngularCompilerPlugin({
tsConfigPath: './tsconfig.json',
entryModule: './src/app/app.module#AppModule'
}),
new MiniCssExtractPlugin({ filename: '[name]-[hash].css', chunkFilename: '[name]-[chunkhash].css' })
],
optimization: {
minimizer: [
new UglifyJsPlugin({
sourceMap: false,
parallel: true,
cache: path.resolve(__dirname, 'webpack-cache/uglify-cache'),
uglifyOptions: {
compress: {
pure_getters: true,
passes: 2
},
output: {
ascii_only: true,
comments: false
}
}
})
],
splitChunks: {
chunks: 'all'
}
},
devServer: {
port: 4201,
host: '127.0.0.1',
historyApiFallback: true,
watchOptions: {
ignored: /node_modules/
}
},
node: {
global: true,
crypto: 'empty',
process: false,
module: false,
clearImmediate: false,
setImmediate: false,
fs: 'empty'
}
}
现在执行命令yarn prod
,以上配置将生成生产模式加AOT模式下的输出,并且最终代码都会压缩至最小体积,像这样:
执行yarn http
运行一下看看:
至此纯手工最简单的angular项目就完成了。对这个小项目做几个总结:
- 其中的配置只针对
prod + AOT
模式,即不是JIT
模式 - 对于开发环境可以再新增一个
webpack.config.dev.js
来配置生产环境下的webpack规则,并搭配webpack-dev-server
使用(本文未完成这一步)。对于生产环境就像文中这样先yarn prod
,然后扔到服务器上。 -
@ngtools/webpack
与MiniCssExtractPlugin
不兼容,所以注意配置exclude
规则。 -
tsconfig.json
中的"module": "esnext"
这一配置相比"module": "commonjs"
能减少不少体积。
相关链接
- angular-starter 一个教科书级复杂的angular启动项目,非常适合进阶观摩学习
- angulectron 本文示例项目地址,之后会整合electron并继续完善。