一般咱們開發vue
項目,都會使用到vue-cli
腳手架工具構建想要的項目模版,這樣當然能無視各類配置,高效地編寫項目。但是vue-cli
這樣相似黑盒的模版配置,對於初學者來講,實際上是很傷的。天天都只會關注業務邏輯代碼,寫了一兩年甚至連webpack
的基本配置都不瞭解。javascript
並且,vue-cli
即便方便,也不能徹底契合咱們的項目,這時咱們就須要手動的構建項目初始模版。爲實現項目的工程化打下基礎。css
項目地址html
master
: webpack4
+ express
+ vue
ts-dev
: webpack4
+ express
+ vue
(vue-class-component
) + typescript
(本文講解)`-- build 構建文件目錄
| |-- configs 項目配置
| |-- appEnvs.js 全局變量配置
| |-- options.js 其餘配置
| |-- proxy.js 服務代理配置
| |-- plugin 插件
| |-- rules 規則
| `-- development.js 開發環境配置
| `-- production.js 生產環境配置
| `-- webpack.base.js 基礎環境配置
|-- public 公共文件夾
`-- src 代碼目錄
|-- @types typescript 聲明文件
|-- http http 文件
|-- assets 多媒體 文件
|-- components 組件 文件
|-- store 狀態 文件
|-- utils 工具 文件
|-- views 視圖 文件夾
| |-- home
| |-- home.module.scss css module 文件
| |-- index.tsx tsx 文件
|-- App.tsx
|-- main.ts 入口文件
|-- router.ts 路由文件
|-- .editorconfig
|-- .prettierrc
|-- .postcssrc.js
|-- babel.config.js
|-- package.json
|-- tsconfig.json
|-- tslint.json
複製代碼
const path = require("path");
// 拋出一些配置, 好比port, builtPath
const config = require("./configs/options");
// css less scss loder 整合
const cssLoaders = require("./rules/cssLoaders");
function resolve(name) {
return path.resolve(__dirname, "..", name);
}
// 開發環境變動不刷新頁面,熱替換
function addDevClient(options) {
if (options.mode === "development") {
Object.keys(options.entry).forEach(name => {
options.entry[name] = [
"webpack-hot-middleware/client?reload=true&noInfo=true"
].concat(options.entry[name]);
});
}
return options.entry;
}
// webpack 配置
module.exports = options => {
const entry = addDevClient({
entry: {
app: [resolve("src/main.ts")]
},
mode: options.mode
});
return {
// Webpack打包的入口
entry: entry,
// 定義webpack如何輸出的選項
output: {
publicPath: "/", // 構建文件的輸出目錄
path: resolve(config.builtPath || "dist"), // 全部輸出文件的目標路徑
filename: "static/js/[name].[hash].js", // 「入口(entry chunk)」文件命名模版
chunkFilename: "static/js/[name].[chunkhash].js" // 非入口(non-entry) chunk 文件的名稱
},
resolve: {
// 模塊的查找目錄
modules: [resolve("node_modules"), resolve("src")],
// 用到的文件的擴展
extensions: [".tsx", ".ts", ".js", ".vue", ".json"],
// 模塊別名列表
alias: {
vue$: "vue/dist/vue.esm.js",
"@components": resolve("src/components"),
"@": resolve("src")
}
},
// 防止將某些 import 的包(package)打包到 bundle 中,
// 而是在運行時(runtime)再去從外部獲取這些擴展依賴(external dependencies)。
// 減小打包後的問價體積。在首頁加入 cdn 引入
externals: {
vue: "Vue",
vuex: "Vuex",
"vue-router": "VueRouter"
},
// 模塊相關配置
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: ["babel-loader"],
exclude: /node_modules/
},
// .tsx 文件的解析
{
test: /(\.tsx)$/,
exclude: /node_modules/,
use: ["babel-loader", "vue-jsx-hot-loader", "ts-loader"]
},
{
test: /(\.ts)$/,
exclude: /node_modules/,
use: ["babel-loader", "ts-loader"]
},
...cssLoaders({
mode: options.mode,
sourceMap: options.sourceMap,
extract: options.mode === "production"
}),
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: "static/img/[name].[hash:7].[ext]"
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: "static/media/[name].[hash:7].[ext]",
fallback: "file-loader"
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: "static/fonts/[name].[hash:7].[ext]"
}
}
]
}
};
};
複製代碼
開發環境配置vue
const webpack = require('webpack')
const path = require('path')
const express = require('express')
const merge = require('webpack-merge')
const chalk = require('chalk')
// 兩個合體實現本地服務熱替換
// 具體實現 https://github.com/webpack-contrib/webpack-hot-middleware
const webpackMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const baseConfig = require('./webpack.base')
const config = require('./configs/options')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const proxyTable = require('./configs/proxy')
// http-proxy-middleware 添加代理
const useExpressProxy = require('./plugins/useExpressProxy')
// 全局變量
const appEnvs = require('./configs/appEnvs')
const app = express()
// 合併 webpack 請求
const compiler = webpack(merge(baseConfig({ mode: 'development' }), {
mode: 'development',
devtool: '#cheap-module-eval-source-map',
// 插件
plugins: [
new ProgressBarPlugin(), // 進度條插件
new FriendlyErrorsWebpackPlugin(),
// 經過 DefinePlugin 來設置 process.env 環境變量的快捷方式。
new webpack.EnvironmentPlugin(appEnvs),
// 模塊熱替換插件, 與 webpack-hot-middleware 配套使用
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: resolve('./public/index.html'),
filename: 'index.html'
})
],
optimization: {
// 跳過生成階段,不會由於錯誤代碼退出
noEmitOnErrors: true
}
}))
function resolve (name) {
return path.resolve(__dirname, '..', name)
}
const devMiddleware = webpackMiddleware(compiler, {
// 同 webpack publicPath
publicPath: '/',
logLevel: 'silent'
})
const hotMiddleware = webpackHotMiddleware(compiler, {
log: false
})
compiler.hooks.compilation.tap('html-webpack-plugin-after-emit', () => {
hotMiddleware.publish({
action: 'reload'
})
})
// 加載中間件
app.use(devMiddleware)
app.use(hotMiddleware)
// 添加代理配置
useExpressProxy(app, proxyTable)
devMiddleware.waitUntilValid(() => {
console.log(chalk.yellow(`I am ready. open http://localhost:${ config.port || 3000 } to see me.`))
})
app.listen(config.port || 3000)
複製代碼
生產環境配置java
const webpack = require('webpack')
const path = require('path')
const ora = require('ora')
const chalk = require('chalk')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
// 替代 extract-text-webpack-plugin, 用於提取 css 文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 用於 css 文件優化壓縮
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 從新構建時清空 dist 文件
const CleanWebpackPlugin = require('clean-webpack-plugin')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const config = require('./configs/options')
const appEnvs = require('./configs/appEnvs')
const compiler = webpack(merge(baseConfig({ mode: 'production' }), {
mode: 'production',
output: {
publicPath: './'
},
performance: {
hints: false
},
plugins: [
new webpack.HashedModuleIdsPlugin(),
new webpack.EnvironmentPlugin(appEnvs),
new webpack.SourceMapDevToolPlugin({
test: /\.js$/,
filename: 'sourcemap/[name].[chunkhash].map',
append: false
}),
new CleanWebpackPlugin([`${config.builtPath || 'dist'}/*`], {
root: path.resolve(__dirname, '..')
}),
new HtmlWebpackPlugin({
template: resolve('./public/index.html'),
filename: 'index.html',
chunks: ['app', 'vendors', 'mainifest'],
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css'
chunkFilename: 'static/css/[id].[contenthash].css'
})
],
optimization: {
// 將webpack運行時生成代碼打包到 mainifest.js
runtimeChunk: {
name: 'mainifest'
},
// 替代 commonChunkPlugin, 拆分代碼
splitChunks: {
chunks: 'async',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
// node_modules 中用到的合併到 vendor.js
vendor: {
test: /node_modules\/(.*)\.js/,
name: 'vendors',
chunks: 'initial',
priority: -10,
reuseExistingChunk: false
},
// 與 mini-css-extract-plugin 配合,將 css 整合到一個文件
styles: {
name: 'styles',
test: /(\.less|\.scss|\.css)$/,
chunks: 'all',
enforce: true,
},
}
},
minimizer: [
// ParallelUglifyPlugin 能夠把對JS文件的串行壓縮變爲開啓多個子進程並行執行
new ParallelUglifyPlugin({
uglifyJS: {
output: {
beautify: false,
comments: false
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
},
cache: true, // 開啓緩存
parallel: true, // 平行壓縮
sourceMap: true // set to true if you want JS source maps
}),
// 壓縮 css
new OptimizeCssAssetsPlugin({
assetNameRegExp: /(\.less|\.scss|\.css)$/g,
cssProcessor: require("cssnano"), // css 壓縮優化器
cssProcessorOptions: {
safe: true,
autoprefixer: { disable: true },
discardComments: { removeAll: true }
}, // 去除全部註釋
canPrint: true
})
]
}
}))
function resolve (name) {
return path.resolve(__dirname, '..', name)
}
const spinner = ora('building for production...').start()
compiler.run((err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete..\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
複製代碼
爲了在 vue
中使用 typescript
, 咱們使用的是 vue-class-component
, 首先咱們須要讓項目兼容 jsx
以及 typescript
node
我使用的是 babel@7
, 相比較 6
, 有些許改動,全部的packages
爲 @babel/xxx
webpack
對於 jsx
的兼容,我直接使用了 @vue/babel-preset-jsx
, 內部加載了 babel-plugin-transform-jsx
git
module.exports = {
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: ['> 1%', 'last 2 versions', 'not ie <= 8']
}
}
],
'@vue/babel-preset-jsx'
],
plugins: [
'@babel/plugin-transform-runtime'
],
comments: false,
env: {
test: {
presets: ['@babel/preset-env'],
plugins: ['babel-plugin-dynamic-import-node']
}
}
}
複製代碼
{
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"],
"compilerOptions": {
// typeRoots option has been previously configured
"typeRoots": [
// add path to @types
"src/@types"
],
"baseUrl": ".",
"paths": {
"*": ["types/*"],
"@/*": ["src/*"]
},
// 以嚴格模式解析
"strict": true,
// 在.tsx文件裏支持JSX
"jsx": "preserve",
// 使用的JSX工廠函數
"jsxFactory": "h",
// 容許從沒有設置默認導出的模塊中默認導入
"allowSyntheticDefaultImports": true,
// 啓用裝飾器
"experimentalDecorators": true,
// "strictFunctionTypes": false,
// 容許編譯javascript文件
"allowJs": true,
// 採用的模塊系統
"module": "esnext",
// 編譯輸出目標 ES 版本
"target": "es5",
// 如何處理模塊
"moduleResolution": "node",
// 在表達式和聲明上有隱含的any類型時報錯
"noImplicitAny": true,
"importHelpers": true,
"lib": ["dom", "es5", "es6", "es7", "es2015.promise"],
"sourceMap": true,
"pretty": true,
"esModuleInterop": true
}
}
複製代碼
{
"defaultSeverity": "warning",
"extends": ["tslint:recommended"],
"linterOptions": {
"exclude": ["node_modules/**"]
},
"allowJs": true,
"rules": {
"arrow-parens": false,
"trailing-comma": false,
"quotemark": [true],
"indent": [true, "spaces", 2],
"interface-name": false,
"ordered-imports": false,
"object-literal-sort-keys": false,
"no-console": false,
"no-debugger": false,
"no-unused-expression": [true, "allow-fast-null-checks"],
"no-unused-variable": false,
"triple-equals": true,
"no-parameter-reassignment": true,
"no-conditional-assignment": true,
"no-construct": true,
"no-duplicate-super": true,
"no-duplicate-switch-case": true,
"no-object-literal-type-assertion": true,
"no-return-await": true,
"no-sparse-arrays": true,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"prefer-object-spread": true,
"radix": true,
"cyclomatic-complexity": [true, 20],
"member-access": false,
"deprecation": false,
"use-isnan": true,
"no-duplicate-imports": true,
"no-mergeable-namespace": true,
"encoding": true,
"import-spacing": true,
"interface-over-type-literal": true,
"new-parens": true,
"no-angle-bracket-type-assertion": true,
"no-consecutive-blank-lines": [true, 3]
}
}
複製代碼
萬事俱備以後,咱們開始編寫 .tsx
文件es6
import { Vue, Component } from "vue-property-decorator";
import { CreateElement } from "vue";
@Component
export default class extends Vue {
// 這裏 h: CreateElement 是重點,沒有就會報錯
// 沒有自動注入h, 涉及到 babel-plugin-transform-jsx 的問題
// 本身也不明白,爲何會沒有自動注入
render(h: CreateElement) {
return (
<div id="app"> <router-view /> </div> ); } } 複製代碼
文章沒有寫的很細,其實不少重要的項目配置尚未加上,好比 commitizen
, lint-stage
, jest
, cypress
, babel-plugin-vue-jsx-sync
, babel-plugin-jsx-v-model
.....github
更多的也是爲了從零開始瞭解體會整個項目的構建過程。
代碼已上傳。歡迎來訪。 項目地址