vue + typescript
以下过程基于vue2.XX,如果使用vue-cli3以上版本,请忽略该过程,不需要配置。。。。直接翻到最后,了解在项目中使用上的出入
安排:
- 安装vue-cli
- 安装ts依赖
- 配置 webpack
- 添加 tsconfig.json
- 添加 tslint.json
- 让 ts 识别 .vue
- 改造 .vue文件
先贴一段最后配置完毕的.vue代码 ,template 和 style 跟以前的写法保持一致,只有 script 的变化
<template>
<div>
<div class="card" ref="hello2">
<div class="title">今天是:{{ date }}</div>
<p class="red">父子组件传参---on和emit</p>
<hello-world :msg="msg" @changeMsg="changeMsg" ref="hello"></hello-world>
</div>
</div>
</template>
<script lang='ts'>
import Vue from "vue";
import Component from "vue-class-component";
import moment from "moment";
import helloWorld from "../components/HelloWorld.vue";
@Component({
name: "indexA",
components: {
helloWorld
}
})
export default class extends Vue {
// 初始化数据
private date = moment().format("YYYY-MM-DD");
msg = "你好么????";
// 声明周期钩子
mounted() {
// alert('你好')
}
public changeMsg(val) {
this.msg = "我不好!!!!" + val;
}
}
</script>
<style></style>
1、Vue 引入 TypeScript
需要安装一些必要/以后需要的插件
npm i vue-class-component vue-property-decorator --save
npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev
说明:
- ==vue-class-component==:强化 Vue 组件,使用 TypeScript/装饰器 增强 Vue 组件
- ==vue-property-decorator==:在 vue-class-component 上增强更多的结合 Vue 特性的装饰器
- ==ts-loader==:TypeScript 为 Webpack 提供了 ts-loader,其实就是为了让webpack识别 .ts .tsx文件
- ==tslint-loader== 跟 ==tslint==:我想你也会在.ts .tsx文件 约束代码格式(作用等同于eslint)
- ==tslint-config-standard==:tslint 配置 standard风格的约束
2、配置 webpack
webpack.base.conf.js 文件
- entry.app 将main.js 改成 main.ts, 顺便把项目文件中的main.js也改成main.ts
entry: {
app: './src/main.ts'
}
- 找到resolve.extensions 里面加上.ts 后缀 (是为了之后引入.ts的时候不写后缀)
resolve: {
extensions: ['.js', '.vue', '.json', '.ts'],
alias: {
'@': resolve('src')
}
}
- 找到module.rules 添加webpack对.ts的解析
{
test: /\.ts$/,
exclude: /node_modules/,
enforce: 'pre',
loader: 'tslint-loader'
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
}
}
说明: ts-loader 会检索当前目录下的 tsconfig.json 文件,根据里面定义的规则来解析.ts文件(就跟.babelrc的作用一样)
3、添加 tsconfig.json
根路径下创建tsconfig.json文件 官方文档配置说明
{
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"allowJs": true,
"module": "esnext",
"target": "es5",
"moduleResolution": "node",
"isolatedModules": true,
"lib": [
"dom",
"es5",
"es2015.promise"
],
"sourceMap": true,
"pretty": true
}
}
4、添加 tslint.json
在根路径下创建tslint.json文件,就是 引入 ts 的 standard 规范 github--说明文件
{
"extends": "tslint-config-standard",
"globals": {
"require": true
}
}
5、让 ts 识别 .vue
由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个 vue-shim.d.ts 文件,放在项目项目对应使用目录下,例如 src/vue-shim.d.ts
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
意思是告诉 TypeScript *.vue 后缀的文件可以交给 vue 模块来处理。
而在代码中导入 *.vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 *.ts 文件,不识别 *.vue 文件:
import Component from 'components/component.vue'
6、改造 .vue 文件
在这之前先让我们了解一下所需要的插件(下面的内容需要掌握es7的装饰器, 就是下面使用的@符号
vue-class-component
vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:
<template>
<div>
<input v-model="msg">
<p>msg: {{ msg }}</p>
<p>computed msg: {{ computedMsg }}</p>
<button @click="greet">Greet</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class App extends Vue {
// 初始化数据
msg = 123
// 声明周期钩子
mounted () {
this.greet()
}
// 计算属性
get computedMsg () {
return 'computed ' + this.msg
}
// 方法
greet () {
alert('greeting: ' + this.msg)
}
}
</script>
上面的代码跟下面的代码作用是一样的
export default {
data () {
return {
msg: 123
}
}
// 声明周期钩子
mounted () {
this.greet()
}
// 计算属性
computed: {
computedMsg () {
return 'computed ' + this.msg
}
}
// 方法
methods: {
greet () {
alert('greeting: ' + this.msg)
}
}
}
vue-property-decorator
vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:
- @Emit
- @Inject
- @Model
- @Prop
- @Provide
- @Watch
- @Component (从 vue-class-component 继承)
在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档
import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'
@Component
export class MyComponent extends Vue {
@Prop()
propA: number = 1
@Prop({ default: 'default value' })
propB: string
@Prop([String, Boolean])
propC: string | boolean
@Prop({ type: null })
propD: any
@Watch('child')
onChildChanged(val: string, oldVal: string) { }
}
上面的代码相当于:
export default {
props: {
checked: Boolean,
propA: Number,
propB: {
type: String,
default: 'default value'
},
propC: [String, Boolean],
propD: { type: null }
}
methods: {
onChildChanged(val, oldVal) { }
},
watch: {
'child': {
handler: 'onChildChanged',
immediate: false,
deep: false
}
}
}
7、开始修改App.vue文件
- 在script 标签上加上 lang="ts", 意思是让webpack将这段代码识别为typescript 而非javascript
- 修改vue组件的构造方式( 跟react组件写法有点类似, 详见官方 ), 如下图
- 用vue-property-decorator语法改造之前代码
踩坑记
1、对webpack版本要求4.0以上
webpack@3.6.0升级至webpack@4.37.0
相关webpack插件都需要升级 webpack-dev-server webpack-cli
Failed to compile.
./src/main.ts
Module build failed: Error: You may be using an old version of webpack; please check you're using at least version 4
at Object.initializeInstance (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\instances.js:175:19)
at successLoader (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\index.js:27:17)
at Object.loader (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\index.js:24:5)
@ multi (webpack)-dev-server/client?http://localhost:7998 webpack/hot/dev-server ./src/main.ts
如果仍然报错,请手动修改package.json中相关webpack版本,然后删除node_modules,重新install
"webpack": "^4.37.0",
"webpack-bundle-analyzer": "^3.7.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-merge": "^4.2.2"
2、compilation.mainTemplate.applyPluginsWaterfall
building for production.../Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/html-webpack-plugin/lib/compiler.js:81
var outputName = compilation.mainTemplate.applyPluginsWaterfall('asset-path', outputOptions.filename, {
^
TypeError: compilation.mainTemplate.applyPluginsWaterfall is not a function
————————————————
- 原因:html-webpack-plugin未升级版本导致
- 解决:升级 html-webpack-plugin 版本
npm i html-webpack-plugin@3.2.0
npm i vue-loader@15.7.1
4、TypeError: Cannot read property ‘eslint’ of undefined
TypeError: Cannot read property 'eslint' of undefined
at Object.module.exports (/Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/eslint-loader/index.js:148:18)
TypeError: Cannot read property 'vue' of undefined
at Object.module.exports (/Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/vue-loader/lib/loader.js:61:18)
@ ./src/main.js 4:0-24 13:21-24
- 原因:eslint-loader、vue-loader版本问题
- 解决:版本升级
npm i eslint-loader@2.2.1 -D
npm i vue-loader@15.7.1 -D
==npm run build==
5、Error:webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead
- 原因:CommonsChunkPlugin已被webpack4废弃,推荐使用SplitChunkPlugin抽离公共模块
- 解决:找到 /build/webpack.prod.conf.js ,去掉如下配置
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
})
更改配置如下,optimization与output同级
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: "all",
minChunks: 2,
maxInitialRequests: 5, // The default limit is too small to showcase the effect
minSize: 0 // This is example is too small to create commons chunks
},
vendor: {
test: /node_modules/,
chunks: "all",
name: "vendor",
priority: 10,
enforce: true
}
}
}
}
6、Error: Path variable [contenthash] not implemented in this context: static/css/[name].[contenthash].css
- 原因:webpack4.x中提取CSS文件应该使用mini-css-extract-plugin,废弃extract-text-webpack-plugin
- 解决:找到 /build/webpack.prod.conf.js ,更改如下配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
// new ExtractTextPlugin({
// filename: utils.assetsPath('css/[name].[contenthash].css'),
// allChunks: true,
// }),
new MiniCssExtractPlugin({
filename: 'css/app.[name].css',
chunkFilename: 'css/app.[contenthash:12].css' // use contenthash *
})
]
再修改/build/utils.js文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
if (options.extract) {
// return ExtractTextPlugin.extract({
// use: loaders,
// fallback: 'vue-style-loader'
// })
return [MiniCssExtractPlugin.loader].concat(loaders)
} else {
return ['vue-style-loader'].concat(loaders)
}
用法出入
1、生命周期
计算属性
<p>computed msg: {{ computedMsg }}</p>
get computedMsg() {
return "computed " + this.msg;
}
beforeCreate() {
console.log("index-----beforeCreate");
}
created() {
console.log("index-----creatd");
}
beforeMount() {
console.log("index-----beforeMount");
}
mounted() {
console.log("index-----mounted");
}
beforeUpdate() {
console.log("index-----beforeUpdate");
}
updated() {
console.log("index-----updated");
}
beforeDestroy() {
console.log("index-----beforeDestroy");
}
destroyed() {
console.log("index-----destroyed");
}
2、路由钩子
需要在app.vue或者main.ts文件中声明路由钩子
import { Component } from 'vue-property-decorator'
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
'beforeRouteUpdate'
])
然后在页面中使用
beforeRouteUpdate(to: any, from: any, next: () => void): void {
console.log("beforeRouteUpdate111", to, from, this);
this.go(to);
next();
}
beforeRouteEnter(to: any, from: any, next: () => void): void {
console.log("beforeRouteEnter111", to, from);
next();
}
beforeRouteLeave(to: any, from: any, next: () => void): void {
console.log("beforeRouteLeave111");
next();
}
或者 监听路由变化
// vue-shim.d.ts 文件
import VueRouter, {Route} from 'vue-router'
declare module 'vue/types/vue' {
interface Vue {
$router: VueRouter; // 这表示this下有这个东西
$route: Route;
}
}
// index.vue
@Watch('$route')
private onChildChanged(val, oldVal) {
console.log(val, oldVal);
}
3、父子组件传参
跟正常一样使用
// 父组件
<template>
<div>
<div class="card">
<p class="red">父子组件传参---on和emit</p>
<hello-world :msg="msg" @changeMsg="changeMsg" ref="hello"></hello-world>
</div>
</div>
</template>
<script lang='ts'>
import Vue from "vue";
import Component from "vue-class-component";
import helloWorld from "../components/HelloWorld.vue";
@Component({
name: "indexA",
components: {
helloWorld
}
})
export default class extends Vue {}
</script>
// 子组件
<template>
<div class="hello">
<h1 @click="changeSay">{{ msg }}</h1>
</div>
</template>
<script lang='ts'>
import Vue from "vue";
import { Component, Prop,Emit } from "vue-property-decorator";
@Component({
name: "helloWorld"
})
export default class extends Vue {
// 初始化数据
@Prop() msg;
num = 6;
changeSay() {
this.num += 1;
this.$emit("changeMsg", this.num);
}
@Emit()
addToCount('changeMsg') {
}
}
</script>
4、插件引入
// main.ts
import share from './tool/share.ts'
import echarts from 'echarts'
Object.defineProperty(Vue.prototype, '$moment', {
value: moment
})
Vue.use(share)
Vue.prototype.$echarts = echarts
// vue-shim.d.ts
declare module 'vue/types/vue' {
interface Vue {
share: any;
$echarts: any;
}
}
5、使用ref
<div class="card" ref="hello2"></div>
// 断言as 就是对this不做校验,表明我知道自己是谁,你不用管
mounted() {
console.log("----11111", (this as any).$refs.hello2.offsetTop);
}