一般咱们开发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
更多的也是为了从零开始了解体会整个项目的构建过程。
代码已上传。欢迎来访。 项目地址